From 87c00bef18a4d8c3eaeea8c19917b97a65d66a42 Mon Sep 17 00:00:00 2001 From: "mayuresh.wani" Date: Thu, 3 Sep 2026 22:49:30 -0700 Subject: [PATCH 1/9] NVIDIA: SAUCE: soc: mediatek: add SSPM control interface Add the SSPM control interface: the SCMI mailbox transport used to talk to the MediaTek System Services Power Manager microcontroller. The transport carries SCMI messages through a shared SRAM message area with doorbell registers, one mailbox pair for the SET and GET channels, with interrupts requested by resource index from the ACPI device that describes the mailbox window. Signed-off-by: mayuresh.wani Signed-off-by: David Cemin --- drivers/soc/mediatek/Kconfig | 9 + drivers/soc/mediatek/Makefile | 1 + drivers/soc/mediatek/sspm_ci/Makefile | 9 + drivers/soc/mediatek/sspm_ci/sspm_ci.c | 354 +++++++++++++++++++++++++ drivers/soc/mediatek/sspm_ci/sspm_ci.h | 13 + 5 files changed, 386 insertions(+) create mode 100644 drivers/soc/mediatek/sspm_ci/Makefile create mode 100644 drivers/soc/mediatek/sspm_ci/sspm_ci.c create mode 100644 drivers/soc/mediatek/sspm_ci/sspm_ci.h diff --git a/drivers/soc/mediatek/Kconfig b/drivers/soc/mediatek/Kconfig index d7293977f06e8..e545399defde6 100644 --- a/drivers/soc/mediatek/Kconfig +++ b/drivers/soc/mediatek/Kconfig @@ -89,4 +89,13 @@ config MTK_SOCINFO information about the SoC to the userspace including the manufacturer name, marketing name and soc name. +config MTK_SSPM_CI + bool "MediaTek SSPM control interface" + depends on ACPI && ARM64 + help + Say yes here to enable the MediaTek SSPM control interface used + by platform firmware communication helpers. + + If unsure, say N. + endmenu diff --git a/drivers/soc/mediatek/Makefile b/drivers/soc/mediatek/Makefile index 0665573e3c4b3..cbeb8291f5da3 100644 --- a/drivers/soc/mediatek/Makefile +++ b/drivers/soc/mediatek/Makefile @@ -9,3 +9,4 @@ obj-$(CONFIG_MTK_MMSYS) += mtk-mmsys.o obj-$(CONFIG_MTK_MMSYS) += mtk-mutex.o obj-$(CONFIG_MTK_SVS) += mtk-svs.o obj-$(CONFIG_MTK_SOCINFO) += mtk-socinfo.o +obj-$(CONFIG_MTK_SSPM_CI) += sspm_ci/ diff --git a/drivers/soc/mediatek/sspm_ci/Makefile b/drivers/soc/mediatek/sspm_ci/Makefile new file mode 100644 index 0000000000000..5d47409a21818 --- /dev/null +++ b/drivers/soc/mediatek/sspm_ci/Makefile @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2026 MediaTek Inc. +# +LOCAL_CHIP_NAME := + +obj-$(CONFIG_MTK_SSPM_CI) += sspm_ci.o + +ccflags-y += -Werror -Wall +ccflags-y += -I$(src)/ diff --git a/drivers/soc/mediatek/sspm_ci/sspm_ci.c b/drivers/soc/mediatek/sspm_ci/sspm_ci.c new file mode 100644 index 0000000000000..db4e677c181bb --- /dev/null +++ b/drivers/soc/mediatek/sspm_ci/sspm_ci.c @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2026 MediaTek Inc. + * + * MediaTek SSPM control interfac driver + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sspm_ci.h" + +/* MBOX */ +#define MBOX_BOX(id, ofs) (sspm_mbox_iomap + (id * 0x10000) + ofs) +#define MBOX_IN_SET(id) (sspm_mbox_iomap + (id * 0x10000) + 0x1000) +#define MBOX_OUT_CLR(id) (sspm_mbox_iomap + (id * 0x10000) + 0x1004) + +/* SCMI */ +#define SCMI_MESSAGE_HEADER_MESSAGE_ID_POS 0 +#define SCMI_MESSAGE_HEADER_MESSAGE_TYPE_POS 8 +#define SCMI_MESSAGE_HEADER_PROTOCOL_ID_POS 10 +#define SCMI_MESSAGE_HEADER_TOKEN_POS 18 + +#define SCMI_NOTIFY_RECV_LEN 20 +#define SCMI_PAYLOAD_OFFSET 7 + +#define SCMI_MESSAGE_HEADER_MESSAGE_ID_MASK \ + ((0xFF) << SCMI_MESSAGE_HEADER_MESSAGE_ID_POS) +#define SCMI_MESSAGE_HEADER_MESSAGE_TYPE_MASK \ + ((0x3) << SCMI_MESSAGE_HEADER_MESSAGE_TYPE_POS) +#define SCMI_MESSAGE_HEADER_PROTOCOL_ID_MASK \ + ((0xFF) << SCMI_MESSAGE_HEADER_PROTOCOL_ID_POS) +#define SCMI_MESSAGE_HEADER_TOKEN_MASK \ + ((0x3FF) << SCMI_MESSAGE_HEADER_TOKEN_POS) + +/* MISC */ +#define SSPM_MBOX_NUM (2) +#define SSPM_CI_SET_ID (0) +#define SSPM_CI_GET_ID (1) + +#define SSPM_CI_MAGIC (0x01) + +/* 10000 iterations × mdelay(1) = 10 seconds max wait for SSPM firmware ack */ +#define SSPM_CI_MAX_POLL_COUNT (10000) + +#define IRQ_NAME_LEN (32) + +enum scmi_tinysys_protocol_cmd { + TINYSYS_COMMON_SET = 0x3, + TINYSYS_COMMON_GET = 0x4, + TINYSYS_POWER_STATE_NOTIFY = 0x5, + TINYSYS_SLBC_CTRL = 0x6, +}; + +enum scmi_mtk_protocol { + SCMI_PROTOCOL_TINYSYS = 0x80, +}; + +enum scmi_message_type { + SCMI_MESSAGE_TYPE_COMMAND = 0, + SCMI_MESSAGE_TYPE_DELAYED_RESPONSE = 2, + SCMI_MESSAGE_TYPE_NOTIFICATION = 3, +}; + +struct scmi_tinysys_common_set_state { + u32 reserv; + u32 feature_id; + u32 p1; + u32 p2; + u32 p3; + u32 p4; + u32 p5; +}; + +struct scmi_msg_t { + u32 reserved0; + u32 status; + u64 reserved1; + u32 flags; + u32 length; + u32 message_header; + u32 payload[7]; +}; + +struct ap_data { + unsigned int irq_mbox[SSPM_MBOX_NUM]; + char irq_name[SSPM_MBOX_NUM][IRQ_NAME_LEN]; +}; + +static void __iomem *sspm_mbox_iomap; + +static struct ap_data apmcu = { + .irq_mbox = {SSPM_CI_SET_ID, + SSPM_CI_GET_ID}, + .irq_name = {"SSPM_CI_SET", + "SSPM_CI_GET"}, +}; + +static struct device *__dev; +static DEFINE_MUTEX(_hw_rsc); +static int _hw_ack; + +/* + * Per-transaction sequence number placed in the SCMI message header token + * field. Incremented under _hw_rsc for every sspm_ci_set() so consecutive + * transactions carry distinct tokens (a hardcoded 0 made all messages + * indistinguishable). Packed as a u8 into the header token field; wraps + * naturally. + */ +static u8 _scmi_token; + +static void sspm_ci_clear_globals(void *data) +{ + mutex_lock(&_hw_rsc); + sspm_mbox_iomap = NULL; + __dev = NULL; + mutex_unlock(&_hw_rsc); +} + +static ssize_t sspm_alive_show(struct device *kobj, + struct device_attribute *attr, char *buf) +{ + int ret; + + ret = sspm_ci_set(7, + 0xDEAD, 0, 0, 0, 0); + + return snprintf(buf, PAGE_SIZE, "%s\n", ret ? "Dead" : "Alive"); +} +DEVICE_ATTR_RO(sspm_alive); + +static uint32_t pack_message_header(u8 message_id, u8 message_type, u8 protocol_id, u8 token) +{ + return ((((message_id) << SCMI_MESSAGE_HEADER_MESSAGE_ID_POS) & + SCMI_MESSAGE_HEADER_MESSAGE_ID_MASK) | + (((message_type) << SCMI_MESSAGE_HEADER_MESSAGE_TYPE_POS) & + SCMI_MESSAGE_HEADER_MESSAGE_TYPE_MASK) | + (((protocol_id) << SCMI_MESSAGE_HEADER_PROTOCOL_ID_POS) & + SCMI_MESSAGE_HEADER_PROTOCOL_ID_MASK) | + (((token) << SCMI_MESSAGE_HEADER_TOKEN_POS) & + SCMI_MESSAGE_HEADER_TOKEN_MASK)); +} + +int sspm_ci_set(u32 feature_id, + u32 p1, u32 p2, u32 p3, u32 p4, u32 p5) +{ + struct scmi_msg_t scmi_tx = {}; + u8 protocol_id, message_type; + u8 token, message_id; + u32 message_header; + u32 count = 0; + + if (!sspm_mbox_iomap) + return -ENODEV; + + scmi_tx.payload[0] = 0; + scmi_tx.payload[1] = feature_id; + scmi_tx.payload[2] = p1; + scmi_tx.payload[3] = p2; + scmi_tx.payload[4] = p3; + scmi_tx.payload[5] = p4; + scmi_tx.payload[6] = p5; + scmi_tx.length = sizeof(struct scmi_tinysys_common_set_state) + sizeof(message_header); + message_id = TINYSYS_COMMON_SET; + protocol_id = SCMI_PROTOCOL_TINYSYS; + message_type = SCMI_MESSAGE_TYPE_COMMAND; + scmi_tx.status = 0; + scmi_tx.flags = 0x1; + + mutex_lock(&_hw_rsc); + + if (!sspm_mbox_iomap) { + mutex_unlock(&_hw_rsc); + return -ENODEV; + } + + /* + * Assign a fresh sequence token per transaction (under _hw_rsc so the + * counter is not raced). Distinct tokens let firmware/response tracing + * tell consecutive commands apart; the previous hardcoded 0 did not. + */ + token = _scmi_token++; + message_header = pack_message_header(message_id, message_type, + protocol_id, token); + scmi_tx.message_header = message_header; + while (readl((void __iomem *)MBOX_IN_SET(SSPM_CI_SET_ID))) { + count++; + if (count > SSPM_CI_MAX_POLL_COUNT) { + mutex_unlock(&_hw_rsc); + dev_err(__dev, "scmi timeout\n"); + return -ETIMEDOUT; + } + mdelay(1); + } + memcpy_toio(MBOX_BOX(SSPM_CI_SET_ID, 0x0), &scmi_tx, sizeof(scmi_tx)); + /* + * Clear any stale ack a previous timed-out transaction may have left + * behind (e.g. a late IRQ that set _hw_ack after that call returned), + * immediately before triggering this send so the ack wait below cannot + * complete prematurely. Clearing here rather than before the (up-to-10s) + * mailbox-free poll above narrows the window in which a late ack from a + * timed-out transaction could be mistaken for this one to just the send + * trigger. It does not fully correlate completions - the handler still + * signals a bare flag without matching the response header/token; that + * correlation is a follow-up change. + */ + WRITE_ONCE(_hw_ack, 0); + writel(SSPM_CI_MAGIC, (void __iomem *)MBOX_IN_SET(SSPM_CI_SET_ID)); + /* Wait sspm-side ack. */ + count = 0; + while (READ_ONCE(_hw_ack) == 0) { + count++; + if (count > SSPM_CI_MAX_POLL_COUNT) { + mutex_unlock(&_hw_rsc); + dev_err(__dev, "scmi timeout\n"); + return -ETIMEDOUT; + } + mdelay(1); + }; + WRITE_ONCE(_hw_ack, 0); + mutex_unlock(&_hw_rsc); + + dev_dbg(__dev, "[%s] fid:%u p1:%u p2:%u p3:%u p4:%u p5:%u\n", + __func__, feature_id, p1, p2, p3, p4, p5); + return 0; +} +EXPORT_SYMBOL(sspm_ci_set); + +static irqreturn_t _mbox_handler_(int irq, void *data) +{ + unsigned int id = *((unsigned int *)data); + + writel(SSPM_CI_MAGIC, (void __iomem *)MBOX_OUT_CLR(id)); + /* + * The word at +0x4 is the mailbox ownership (mem_free) flag of the + * vendor transport, not an SCMI return status: TINYSYS SET carries no + * status word and the reference driver clears this flag the same way + * to hand the slot back to the sender. Firmware results, where a + * command has any, are returned in the reply payload of GET. + */ + writel(0x0, (void __iomem *)MBOX_BOX(id, 0x4)); + + /* + * Only the SET mailbox completes an sspm_ci_set() ack wait. The GET + * mailbox shares this handler but carries unsolicited firmware + * notifications; acking on it would let a GET interrupt complete an + * in-flight SET transaction early (before firmware consumed the SET + * message), so the ack is gated to the SET id. + */ + if (id == SSPM_CI_SET_ID) + WRITE_ONCE(_hw_ack, 1); + + return IRQ_HANDLED; +} + +static int sspm_ci_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + void __iomem *base; + int id = 0, irq = 0, ret = 0; + struct ap_data *data = &apmcu; + struct resource *res; + + /* + * Use devm_ioremap() rather than devm_platform_ioremap_resource(): + * the latter reserves the region via request_mem_region(), which fails + * with -EBUSY here because the ACPI core has already claimed the _CRS + * memory resource (see /proc/iomem "NVDA8400:00"). devm_ioremap() maps + * without exclusive reservation while still being devres-managed. + */ + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -ENODEV; + base = devm_ioremap(dev, res->start, resource_size(res)); + if (!base) + return -ENOMEM; + + mutex_lock(&_hw_rsc); + sspm_mbox_iomap = base; + __dev = dev; + mutex_unlock(&_hw_rsc); + + ret = devm_add_action_or_reset(dev, sspm_ci_clear_globals, NULL); + if (ret) + return ret; + + for (id = 0; id < SSPM_MBOX_NUM; id++) { + /* Get mbox device information. */ + irq = platform_get_irq(pdev, data->irq_mbox[id]); + if (irq < 0) { + dev_err(dev, "platform_get_irq(%d) fail.\n", id); + return irq; + } + + /* Register mbox handler. */ + ret = devm_request_irq(dev, irq, _mbox_handler_, + IRQF_TRIGGER_NONE | IRQF_NO_SUSPEND, data->irq_name[id], + &data->irq_mbox[id]); + if (ret) { + dev_err(dev, "request_irq(%s) fail=%d.\n", data->irq_name[id], ret); + return ret; + } + dev_info(dev, "%s IRQ = %d.\n", data->irq_name[id], irq); + } + + return 0; +} + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0) +static void sspm_ci_remove(struct platform_device *pdev) +#else +static int sspm_ci_remove(struct platform_device *pdev) +#endif +{ + /* + * Resources are devm-managed. Probe registration order makes teardown: + * IRQs -> clear globals -> unmap MMIO. + */ +#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 14, 0) + return 0; +#endif +} + +static const struct acpi_device_id sspm_ci_acpi_match[] = { + {"MTKW8400", 0, 0, 0}, + {"NVDA8400", 0, 0, 0}, + {}, +}; +MODULE_DEVICE_TABLE(acpi, sspm_ci_acpi_match); + +static struct platform_driver sspm_ci_driver = { + .probe = sspm_ci_probe, + .remove = sspm_ci_remove, + .driver = { + .name = "SSPM_CI", + .acpi_match_table = ACPI_PTR(sspm_ci_acpi_match), + }, +}; + +static int __init sspm_ci_init(void) +{ + return platform_driver_register(&sspm_ci_driver); +} + +subsys_initcall(sspm_ci_init); + +MODULE_DESCRIPTION("MediaTek SSPM control interface driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/soc/mediatek/sspm_ci/sspm_ci.h b/drivers/soc/mediatek/sspm_ci/sspm_ci.h new file mode 100644 index 0000000000000..9df540f70a67a --- /dev/null +++ b/drivers/soc/mediatek/sspm_ci/sspm_ci.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (C) 2026 MediaTek Inc. + * + * MediaTek SSPM control interface driver + */ + +#ifndef _SSPM_CI_H_ +#define _SSPM_CI_H_ + +int sspm_ci_set(u32 feature_id, + u32 p1, u32 p2, u32 p3, u32 p4, u32 p5); +#endif /* _SSPM_CI_H_ */ From b141068c067c7a67ed47abc9d2ede9dd2523a216 Mon Sep 17 00:00:00 2001 From: "mayuresh.wani" Date: Thu, 3 Sep 2026 22:49:30 -0700 Subject: [PATCH 2/9] NVIDIA: SAUCE: soc: mediatek: add power_wrap device power control driver Add the power_wrap driver: ACPI-driven device power control through the SSPM control interface. Kernel clients and the ACPI power flows use it to request device power, clock and mode changes from the SSPM firmware, which reference counts the requests across operating system components. The public client header is introduced directly at include/linux/soc/mediatek/mtk-pwrap.h so in-tree consumers outside drivers/soc can include it without relative paths. Signed-off-by: mayuresh.wani Co-developed-by: David Cemin Signed-off-by: David Cemin --- drivers/soc/mediatek/Kconfig | 9 + drivers/soc/mediatek/Makefile | 1 + drivers/soc/mediatek/power_wrap/Makefile | 9 + drivers/soc/mediatek/power_wrap/acpiutil.c | 429 +++++++++++++++++ .../soc/mediatek/power_wrap/inc/power_wrap.h | 154 ++++++ .../soc/mediatek/power_wrap/inc/pwrap_scmi.h | 81 ++++ drivers/soc/mediatek/power_wrap/power_wrap.c | 448 ++++++++++++++++++ include/linux/soc/mediatek/mtk-pwrap.h | 73 +++ 8 files changed, 1204 insertions(+) create mode 100644 drivers/soc/mediatek/power_wrap/Makefile create mode 100644 drivers/soc/mediatek/power_wrap/acpiutil.c create mode 100644 drivers/soc/mediatek/power_wrap/inc/power_wrap.h create mode 100644 drivers/soc/mediatek/power_wrap/inc/pwrap_scmi.h create mode 100644 drivers/soc/mediatek/power_wrap/power_wrap.c create mode 100644 include/linux/soc/mediatek/mtk-pwrap.h diff --git a/drivers/soc/mediatek/Kconfig b/drivers/soc/mediatek/Kconfig index e545399defde6..b20141abb96ee 100644 --- a/drivers/soc/mediatek/Kconfig +++ b/drivers/soc/mediatek/Kconfig @@ -96,6 +96,15 @@ config MTK_SSPM_CI Say yes here to enable the MediaTek SSPM control interface used by platform firmware communication helpers. + If unsure, say N. +config MTK_POWER_WRAP + bool "MediaTek Power Wrap ACPI device-control support" + depends on ACPI && ARM64 + depends on MTK_SSPM_CI + help + Say yes here to enable MediaTek Power Wrap support for ACPI based + device power-control flows. + If unsure, say N. endmenu diff --git a/drivers/soc/mediatek/Makefile b/drivers/soc/mediatek/Makefile index cbeb8291f5da3..91d8b58d9faff 100644 --- a/drivers/soc/mediatek/Makefile +++ b/drivers/soc/mediatek/Makefile @@ -10,3 +10,4 @@ obj-$(CONFIG_MTK_MMSYS) += mtk-mutex.o obj-$(CONFIG_MTK_SVS) += mtk-svs.o obj-$(CONFIG_MTK_SOCINFO) += mtk-socinfo.o obj-$(CONFIG_MTK_SSPM_CI) += sspm_ci/ +obj-$(CONFIG_MTK_POWER_WRAP) += power_wrap/ diff --git a/drivers/soc/mediatek/power_wrap/Makefile b/drivers/soc/mediatek/power_wrap/Makefile new file mode 100644 index 0000000000000..3a551b55b31c7 --- /dev/null +++ b/drivers/soc/mediatek/power_wrap/Makefile @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0 + +# Composite name power_wrap_drv avoids colliding with the source file power_wrap.o +obj-y += power_wrap_drv.o +power_wrap_drv-y += power_wrap.o +power_wrap_drv-y += acpiutil.o + +ccflags-y += -Wall +ccflags-y += -I$(src)/inc diff --git a/drivers/soc/mediatek/power_wrap/acpiutil.c b/drivers/soc/mediatek/power_wrap/acpiutil.c new file mode 100644 index 0000000000000..bcfdd8967da03 --- /dev/null +++ b/drivers/soc/mediatek/power_wrap/acpiutil.c @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2026 MediaTek Inc. + * + * acpiutil.c - ACPI utility functions for power_wrap driver + * + * This module builds the device-control (DPM) table from the PEPD + * device's ACPI _DSM method (UUID 64B66B51), including the per-device + * SCMI dev_id fetched via GDSC(). + */ + +#include +#include +#include +#include + +#include "power_wrap.h" + +/* ACPI NameSeg is always exactly 4 characters, trailing-padded with '_' */ +#define PWRAP_ACPI_NAMESEG_LEN 4 + +/* ============================================================ + * DPM UUID (64B66B51) - Device-control config from ACPI + * ============================================================ + * + * Builds the device-control table at probe by parsing the DPMT package + * returned by UUID 64B66B51 function 1 (Case 1 in Cpu.asl). + * + * Note: the DPMT package does NOT carry the per-device SCMI dev_id; that + * lives in a separate ASL method, GDSC(path), which writes the global + * SRTP package. So for CTRL_BY_SCMI devices we make a second ACPI call to + * GDSC() to obtain dev_id. + */ + +/* GDSC return package (SRTP) layout - see Cpu.asl Name(SRTP, ...) */ +enum pwrap_gdsc_srtp_idx { + GDSC_SRTP_FEAT_ID = 0, /* SCMI feature id (0x0D; 0xFFFFFFFF = unsupported) */ + GDSC_SRTP_DEV_ID, /* SCMI device id (the per-device value we need) */ + GDSC_SRTP_PREPARE, /* Package(4): prepare action payloads */ + GDSC_SRTP_DSTATE, /* Package(4): d-state action payloads */ + GDSC_SRTP_FSTATE, /* Package(4): f-state action payloads */ + GDSC_SRTP_REQ, /* Package(4): request action payloads */ + GDSC_SRTP_ABANDON, /* Package(4): abandon action payloads */ + GDSC_SRTP_COUNT, +}; + +#define GDSC_FEAT_ID_UNSUPPORTED 0xFFFFFFFFU + +/* + * pwrap_acpi_normalize_path() - Pad ACPI NameSegs to 4 chars with '_' + * + * ACPI NameSegs are fixed at 4 bytes; names shorter than 4 chars are + * trailing-padded with '_' (ACPI spec 19.2.2 / 5.3). The kernel's + * acpi_get_name(ACPI_FULL_PATHNAME) - used to key device lookups - emits + * the padded form ("\_SB_.PCI0.RP0_"), but the _DSM string literals are + * written unpadded ("\_SB.PCI0.RP0"). Normalizing makes the two agree. + * + * Leading root '\' and parent '^' prefix chars are copied verbatim; each + * '.'-separated NameSeg is then padded to 4 chars. + */ +int pwrap_acpi_normalize_path(const char *in, char *out, size_t out_sz) +{ + size_t o = 0; + const char *p = in; + + if (!in || !out || out_sz == 0) + return -EINVAL; + + /* Copy leading root/parent prefix ('\' and any '^') verbatim. */ + while (*p == '\\' || *p == '^') { + if (o + 1 >= out_sz) + return -ENOSPC; + out[o++] = *p++; + } + + /* Process each '.'-separated NameSeg, padding short ones to 4. */ + while (*p) { + size_t seg_len = strcspn(p, "."); + size_t pad = (seg_len < PWRAP_ACPI_NAMESEG_LEN) ? + (PWRAP_ACPI_NAMESEG_LEN - seg_len) : 0; + + if (o + seg_len + pad + 1 >= out_sz) + return -ENOSPC; + + memcpy(&out[o], p, seg_len); + o += seg_len; + while (pad--) + out[o++] = '_'; + + p += seg_len; + if (*p == '.') { + out[o++] = '.'; + p++; + } + } + + out[o] = '\0'; + return 0; +} + +/* + * pwrap_acpi_fetch_scmi_dev_id() - Get per-device SCMI dev_id via GDSC() + * @dev: PEPD ACPI device + * @raw_path: UNPADDED device path as it appears in the _DSM literal + * (GDSC's ASL ElseIf compares against unpadded names) + * @dev_id_out: receives the SCMI device id on success + * + * Evaluates \_SB.PEPD.GDSC(raw_path), which returns the SRTP package + * {feat_id, dev_id, <5 action payload packages>}. We only consume dev_id; + * feat_id and the action IDs are invariant and supplied by SCMI_CONFIG_INIT(). + * + * Return: 0 on success, -ENODEV if the firmware reports the path as + * unsupported (feat_id == 0xFFFFFFFF), negative errno otherwise. + */ +static int pwrap_acpi_fetch_scmi_dev_id(struct device *dev, + const char *raw_path, u32 *dev_id_out) +{ + acpi_handle handle = ACPI_HANDLE(dev); + struct acpi_object_list args; + union acpi_object arg, *pkg, *feat, *devid; + struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + int ret = 0; + + arg.type = ACPI_TYPE_STRING; + arg.string.pointer = (char *)raw_path; + arg.string.length = strlen(raw_path); + args.count = 1; + args.pointer = &arg; + + status = acpi_evaluate_object(handle, "GDSC", &args, &out); + if (ACPI_FAILURE(status)) { + dev_warn(dev, "GDSC(%s) evaluation failed: %s\n", + raw_path, acpi_format_exception(status)); + return -EIO; + } + + pkg = out.pointer; + if (!pkg || pkg->type != ACPI_TYPE_PACKAGE || + pkg->package.count < GDSC_SRTP_COUNT) { + dev_warn(dev, "GDSC(%s): unexpected return (type/count)\n", + raw_path); + ret = -EINVAL; + goto out_free; + } + + feat = &pkg->package.elements[GDSC_SRTP_FEAT_ID]; + devid = &pkg->package.elements[GDSC_SRTP_DEV_ID]; + if (feat->type != ACPI_TYPE_INTEGER || + devid->type != ACPI_TYPE_INTEGER) { + dev_warn(dev, "GDSC(%s): feat_id/dev_id not integers\n", + raw_path); + ret = -EINVAL; + goto out_free; + } + + if ((u32)feat->integer.value == GDSC_FEAT_ID_UNSUPPORTED) { + dev_warn(dev, "GDSC(%s): firmware reports path unsupported\n", + raw_path); + ret = -ENODEV; + goto out_free; + } + + *dev_id_out = (u32)devid->integer.value; + +out_free: + ACPI_FREE(out.pointer); + return ret; +} + +/* + * pwrap_parse_constraints() - Flatten the constraints sub-package + * + * ASL forms (Cpu.asl Case 1): + * Package(0x3){enable, device_state, comp_count} + * Package(0x4){enable, device_state, comp_count, Package(){comp_states...}} + * + * Maps onto pwrap_idle_constraints.values[] = {enable, device_state, + * comp_count, comp_states[0..comp_count-1]}. + */ +static void pwrap_parse_constraints(struct device *dev, + union acpi_object *pkg, + struct pwrap_idle_constraints *c) +{ + u32 i, n; + + memset(c, 0, sizeof(*c)); + + if (!pkg || pkg->type != ACPI_TYPE_PACKAGE) + return; + + n = pkg->package.count; + if (n > PWRAP_IDLE_CONSTRAINT_SCALARS && + pkg->package.elements[PWRAP_IDLE_CONSTRAINT_SCALARS].type == ACPI_TYPE_PACKAGE) { + /* 4-element form: scalars + nested comp_states package */ + union acpi_object *comp = + &pkg->package.elements[PWRAP_IDLE_CONSTRAINT_SCALARS]; + u32 j; + + for (i = 0; i < PWRAP_IDLE_CONSTRAINT_SCALARS; i++) { + union acpi_object *e = &pkg->package.elements[i]; + + if (e->type == ACPI_TYPE_INTEGER) + c->values[i] = e->integer.value; + } + for (j = 0; j < comp->package.count && + (PWRAP_IDLE_CONSTRAINT_SCALARS + j) < + (PWRAP_MAX_COMP_NUM + PWRAP_IDLE_CONSTRAINT_SCALARS); j++) { + union acpi_object *e = &comp->package.elements[j]; + + if (e->type == ACPI_TYPE_INTEGER) + c->values[PWRAP_IDLE_CONSTRAINT_SCALARS + j] = + e->integer.value; + } + } else { + /* Flat form: copy each integer scalar in order */ + for (i = 0; i < n && + i < (PWRAP_MAX_COMP_NUM + PWRAP_IDLE_CONSTRAINT_SCALARS); i++) { + union acpi_object *e = &pkg->package.elements[i]; + + if (e->type == ACPI_TYPE_INTEGER) + c->values[i] = e->integer.value; + } + } + + /* Clamp comp_count to the number of entries actually written into + * comp_states[]. Firmware may report a larger value than PWRAP_MAX_COMP_NUM + * but the parse loops above cap writes at that limit. Without this clamp, + * consumers iterating comp_states[] up to comp_count would read beyond the + * array bounds into adjacent struct fields. + */ + if (c->comp_count > PWRAP_MAX_COMP_NUM) + c->comp_count = PWRAP_MAX_COMP_NUM; +} + +/* + * pwrap_parse_dev_package() - Parse one DPMT device package into a config + * + * Package layout (DPM_DEV_* enum): {path, type, constraints, control_type}. + * On success @cfg->device_path / @cfg->device_type are kstrdup'd (freed by + * pwrap_acpi_dpm_config_free()). + */ +static int pwrap_parse_dev_package(struct device *dev, + union acpi_object *pkg, + struct pwrap_dev_config *cfg) +{ + union acpi_object *path_obj, *type_obj, *ctrl_obj; + char norm_path[PWRAP_DEV_PATH_MAX]; + const char *raw_path; + u64 control_type; + int ret; + + if (pkg->type != ACPI_TYPE_PACKAGE || + pkg->package.count < DPM_DEV_PKG_COUNT) { + dev_warn(dev, "DPMT device package: bad type/count\n"); + return -EINVAL; + } + + path_obj = &pkg->package.elements[DPM_DEV_PATH]; + type_obj = &pkg->package.elements[DPM_DEV_TYPE]; + ctrl_obj = &pkg->package.elements[DPM_DEV_CONTROL_TYPE]; + + if (path_obj->type != ACPI_TYPE_STRING || !path_obj->string.pointer || + ctrl_obj->type != ACPI_TYPE_INTEGER) { + dev_warn(dev, "DPMT device package: bad path/control_type\n"); + return -EINVAL; + } + raw_path = path_obj->string.pointer; + + /* Normalize the (unpadded) _DSM path into the padded lookup key. */ + ret = pwrap_acpi_normalize_path(raw_path, norm_path, sizeof(norm_path)); + if (ret) { + dev_warn(dev, "DPMT: cannot normalize path \"%s\": %d\n", + raw_path, ret); + return ret; + } + + cfg->device_path = kstrdup(norm_path, GFP_KERNEL); + if (!cfg->device_path) + return -ENOMEM; + + if (type_obj->type == ACPI_TYPE_STRING && type_obj->string.pointer) { + cfg->device_type = kstrdup(type_obj->string.pointer, GFP_KERNEL); + if (!cfg->device_type) { + kfree(cfg->device_path); + cfg->device_path = NULL; + return -ENOMEM; + } + } + + pwrap_parse_constraints(dev, &pkg->package.elements[DPM_DEV_CONSTRAINTS], + &cfg->constraints); + + control_type = ctrl_obj->integer.value; + switch (control_type) { + case CTRL_NONE: + cfg->control_type = CTRL_NONE; + break; + case CTRL_BY_PWRAP: + cfg->control_type = CTRL_BY_PWRAP; + break; + case CTRL_BY_SCMI: + cfg->control_type = CTRL_BY_SCMI; + break; + default: + dev_warn(dev, "DPMT: \"%s\" unknown control_type %llu -> NONE\n", + norm_path, control_type); + cfg->control_type = CTRL_NONE; + break; + } + + /* + * SCMI devices need the per-device dev_id, which is not in the DPMT. + * Seed the invariant SCMI payload (feat_id + action IDs) the same way + * the static table does, then fetch dev_id from GDSC() using the RAW + * (unpadded) path the ASL compares against. + */ + if (cfg->control_type == CTRL_BY_SCMI) { + struct pwrap_scmi_config seed = SCMI_CONFIG_INIT(0); + u32 dev_id = 0; + + cfg->scmi_config = seed; + + ret = pwrap_acpi_fetch_scmi_dev_id(dev, raw_path, &dev_id); + if (ret) { + dev_warn(dev, "DPMT: \"%s\" SCMI but no dev_id (%d); " + "demoting to CTRL_NONE\n", norm_path, ret); + cfg->control_type = CTRL_NONE; + } else { + cfg->scmi_config.dev_id = dev_id; + dev_info(dev, "DPMT: \"%s\" SCMI dev_id=0x%x\n", + norm_path, dev_id); + } + } + + return 0; +} + +int pwrap_acpi_fetch_dpm_config(struct device *dev, + struct pwrap_dev_ctrl *ctrl) +{ + acpi_handle handle = ACPI_HANDLE(dev); + union acpi_object *obj, *rev_obj, *cnt_obj; + u64 count; + u32 i; + int ret = 0; + + if (!dev || !ctrl) + return -EINVAL; + + obj = acpi_evaluate_dsm(handle, &pwrap_dsm_dpm_uuid, + DSM_DPM_REV_0, DSM_DPM_GET_DEV_CTRL_INFO, NULL); + if (!obj) { + dev_err(dev, "Failed to evaluate DPM _DSM func %d\n", + DSM_DPM_GET_DEV_CTRL_INFO); + return -EINVAL; + } + + if (obj->type != ACPI_TYPE_PACKAGE || + obj->package.count <= DSM_DPM_DPMT_DCFG) { + dev_err(dev, "DPMT: expected package with devices, got type %d count %d\n", + obj->type, obj->type == ACPI_TYPE_PACKAGE ? + obj->package.count : -1); + ret = -EINVAL; + goto out_free; + } + + rev_obj = &obj->package.elements[DSM_DPM_DPMT_REV]; + cnt_obj = &obj->package.elements[DSM_DPM_DPMT_COUNT]; + if (rev_obj->type != ACPI_TYPE_INTEGER || + cnt_obj->type != ACPI_TYPE_INTEGER) { + dev_err(dev, "DPMT: revision/count not integers\n"); + ret = -EINVAL; + goto out_free; + } + + count = cnt_obj->integer.value; + if (count != obj->package.count - DSM_DPM_DPMT_DCFG) { + dev_err(dev, "DPMT: count %llu != package devices %u\n", + count, obj->package.count - DSM_DPM_DPMT_DCFG); + ret = -EINVAL; + goto out_free; + } + + ctrl->configs = kcalloc(count, sizeof(*ctrl->configs), GFP_KERNEL); + if (!ctrl->configs) { + ret = -ENOMEM; + goto out_free; + } + ctrl->revision = rev_obj->integer.value; + ctrl->count = count; + + dev_info(dev, "DPM config from ACPI: revision=%llu, %llu devices\n", + ctrl->revision, count); + + for (i = 0; i < count; i++) { + union acpi_object *pkg = + &obj->package.elements[DSM_DPM_DPMT_DCFG + i]; + + ret = pwrap_parse_dev_package(dev, pkg, &ctrl->configs[i]); + if (ret) { + dev_err(dev, "DPMT: failed to parse device %u: %d\n", + i, ret); + pwrap_acpi_dpm_config_free(ctrl); + goto out_free; + } + } + +out_free: + ACPI_FREE(obj); + return ret; +} + +void pwrap_acpi_dpm_config_free(struct pwrap_dev_ctrl *ctrl) +{ + u64 i; + + if (!ctrl || !ctrl->configs) + return; + + for (i = 0; i < ctrl->count; i++) { + kfree(ctrl->configs[i].device_path); + kfree(ctrl->configs[i].device_type); + } + kfree(ctrl->configs); + ctrl->configs = NULL; + ctrl->count = 0; + ctrl->revision = 0; +} diff --git a/drivers/soc/mediatek/power_wrap/inc/power_wrap.h b/drivers/soc/mediatek/power_wrap/inc/power_wrap.h new file mode 100644 index 0000000000000..ee0181dab1cb7 --- /dev/null +++ b/drivers/soc/mediatek/power_wrap/inc/power_wrap.h @@ -0,0 +1,154 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2026 MediaTek Inc. + */ +#ifndef __MTK_PWRAP_H__ +#define __MTK_PWRAP_H__ + +#include +#include + +#include "pwrap_scmi.h" +#include + +#define PWRAP_MAX_COMP_NUM 20 +/* Number of scalar constraint fields before the comp_states array */ +#define PWRAP_IDLE_CONSTRAINT_SCALARS 3 + +/* + * Max length of a normalized ACPI device path string. + * The longest path on MT8901 is "\_SB_.PCI2.RP0_.NVM_" (20 chars); + * 64 leaves ample headroom for deeper hierarchies. + */ +#define PWRAP_DEV_PATH_MAX 64 + +extern const guid_t pwrap_dsm_dpm_uuid; + +enum pwrap_control_type { + CTRL_NONE = 0, + CTRL_BY_PWRAP = 1, + CTRL_BY_SCMI = 2, +}; + +enum pwrap_dsm_dpm_func_rev { + DSM_DPM_REV_0 = 0, +}; + +enum pwrap_dsm_dpm_func_idx { + DSM_DPM_GET_SUPPORT_FUNC = 0, + DSM_DPM_GET_DEV_CTRL_INFO, +}; + +enum pwrap_dsm_dpm_dpmt_idx { + DSM_DPM_DPMT_REV = 0, + DSM_DPM_DPMT_COUNT, + DSM_DPM_DPMT_DCFG, +}; + +/* + * Field layout of one device package inside the DPMT (DSM_DPM_DPMT_DCFG+). + * ASL (Cpu.asl, UUID 64B66B51, Case 1): + * Package(){ "", "", Package(){constraints}, } + */ +enum pwrap_dpm_dev_pkg_idx { + DPM_DEV_PATH = 0, + DPM_DEV_TYPE, + DPM_DEV_CONSTRAINTS, + DPM_DEV_CONTROL_TYPE, + DPM_DEV_PKG_COUNT, +}; + +struct pwrap_idle_constraints { + union { + struct { + u64 enable; + u64 device_state; + u64 comp_count; + u64 comp_states[PWRAP_MAX_COMP_NUM]; + }; + u64 values[PWRAP_MAX_COMP_NUM + 3]; + }; +}; + +/* Device configuration data structure */ +struct pwrap_dev_config { + char *device_path; + char *device_type; + struct pwrap_idle_constraints constraints; + enum pwrap_control_type control_type; + struct pwrap_scmi_config scmi_config; +}; + +/* Device control structure */ +struct pwrap_dev_ctrl { + u64 revision; + u64 count; + struct pwrap_dev_config *configs; +}; + +struct mtk_pwrap_device { + const char *name; + unsigned int dev_id; +}; + +/* Driver private data */ +struct pwrap_driver_data { + struct device *dev; + //struct pwrap_dev_ctrl dev_ctrl; + struct platform_device *pdev; + struct acpi_buffer acpi_path; +}; + +/* Create the sysfs debug/test nodes (non-fatal; group is devm-managed) */ +int pwrap_create_sys_files(struct platform_device *pdev); + +/* Getter for static pwrap_dev_ctrl */ +struct pwrap_dev_ctrl *pwrap_get_dev_ctrl(void); +void *pwrap_query_dev_config(const char *dev_path); + +/* ============================================================ + * DPM device-control config from ACPI (UUID 64B66B51) + * ============================================================ + * + * Implemented in acpiutil.c. Declared here because they operate on + * struct pwrap_dev_ctrl / pwrap_scmi_config, which are defined above. + */ + +/** + * pwrap_acpi_normalize_path() - Pad ACPI NameSegs to 4 chars with '_' + * @in: source path as written in the _DSM literal (e.g. "\\_SB.DSP1") + * @out: destination buffer of size @out_sz (>= PWRAP_DEV_PATH_MAX) + * @out_sz: size of @out + * + * ACPI NameSegs are fixed 4 bytes, short names trailing-padded with '_' + * (ACPI spec 19.2.2 / 5.3). acpi_get_name(ACPI_FULL_PATHNAME) emits the + * padded form, but _DSM string literals are unpadded. Normalizing makes + * the fetched path match the keys used by pwrap_query_dev_config(). + * + * Return: 0 on success, -EINVAL on bad input, -ENOSPC if too long. + */ +int pwrap_acpi_normalize_path(const char *in, char *out, size_t out_sz); + +/** + * pwrap_acpi_fetch_dpm_config() - Populate pwrap_dev_ctrl from DPM _DSM + * @dev: PEPD ACPI device + * @ctrl: device-control table to fill (configs[] is kalloc'd here) + * + * Evaluates UUID 64B66B51 function 1 (DPMT), parses every device package + * (path/type/constraints/control_type), normalizes the path, and for + * CTRL_BY_SCMI devices fetches the per-device SCMI dev_id via GDSC(). + * + * Return: 0 on success, negative errno on failure. + */ +int pwrap_acpi_fetch_dpm_config(struct device *dev, + struct pwrap_dev_ctrl *ctrl); + +/** + * pwrap_acpi_dpm_config_free() - Free a pwrap_dev_ctrl built from ACPI + * @ctrl: table previously filled by pwrap_acpi_fetch_dpm_config() + * + * Frees each kstrdup'd device_path/device_type and the configs[] array. + */ +void pwrap_acpi_dpm_config_free(struct pwrap_dev_ctrl *ctrl); + +#endif diff --git a/drivers/soc/mediatek/power_wrap/inc/pwrap_scmi.h b/drivers/soc/mediatek/power_wrap/inc/pwrap_scmi.h new file mode 100644 index 0000000000000..fe924e8e341b4 --- /dev/null +++ b/drivers/soc/mediatek/power_wrap/inc/pwrap_scmi.h @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2026 MediaTek Inc. + */ +#ifndef __MTK_PWRAP_SCMI_H__ +#define __MTK_PWRAP_SCMI_H__ + +#include +#include + +#ifdef CONFIG_MTK_SSPM_CI +int sspm_ci_set(u32 feature_id, + u32 p1, u32 p2, u32 p3, u32 p4, u32 p5); +#else +static inline int sspm_ci_set(u32 feature_id, + u32 p1, u32 p2, u32 p3, u32 p4, u32 p5) +{ + return -ENODEV; +} +#endif /* CONFIG_MTK_SSPM_CI */ + +#define PWRAP_SCMI_DEV_ACT_ID(_a) ((_a) & (U8_MAX << 24)) +#define PWRAP_SCMI_DEV_STATE(_s) (((u8) (_s) & U8_MAX) << 16) +#define PWRAP_SCMI_DEV_DRIV_CMPLT(_c) (((u8) (_c) & U8_MAX) << 8) +#define PWRAP_SCMI_SYS_TRANS(_t) ((u8) (_t) & U8_MAX) + +#define PWRAP_SCMI_DEV_ACT_PAYLOAD(_a, _s, _c, _t) \ + (PWRAP_SCMI_DEV_ACT_ID(_a) | PWRAP_SCMI_DEV_STATE(_s) | \ + PWRAP_SCMI_DEV_DRIV_CMPLT(_c) | PWRAP_SCMI_SYS_TRANS(_t)) + +#define PWRAP_SCMI_COM_ACT_ID(_a) ((_a) & (U8_MAX << 24)) +#define PWRAP_SCMI_COM_IDX(_c) (((u8) (_c) & U8_MAX) << 16) +#define PWRAP_SCMI_COM_STATE(_s) (((u8) (_s) & U8_MAX) << 8) +#define PWRAP_COM_DRIV_NOTIFIED(_d) ((u8) (_d) & U8_MAX) + +#define PWRAP_SCMI_COM_ACT_PAYLOAD(_a, _c, _s, _d) \ + (PWRAP_SCMI_COM_ACT_ID(_a) | PWRAP_SCMI_COM_IDX(_c) | \ + PWRAP_SCMI_COM_STATE(_s) | PWRAP_COM_DRIV_NOTIFIED(_d)) + +#define SCMI_PROBE_ACTION_ID 0x01000000 +#define SCMI_DEV_STATE_ACTION_ID 0x02000000 +#define SCMI_COM_IDLE_ACTION_ID 0x03000000 +#define SCMI_DEV_REQUEST_ACTION_ID 0x04000000 +#define SCMI_DEV_ABANDON_ACTION_ID 0x05000000 + +#define SCMI_CONFIG_INIT(_dev) { \ + .feat_id = 0x0D, \ + .dev_id = _dev, \ + .data = { \ + [SCMI_DEV_PROBE] = {SCMI_PROBE_ACTION_ID}, \ + [SCMI_DEV_STATE] = {SCMI_DEV_STATE_ACTION_ID}, \ + [SCMI_COM_IDLE] = {SCMI_COM_IDLE_ACTION_ID}, \ + [SCMI_DEV_REQUEST] = {SCMI_DEV_REQUEST_ACTION_ID}, \ + [SCMI_DEV_ADANDON] = {SCMI_DEV_ABANDON_ACTION_ID}, \ + } \ + } + +enum mtk_pwrap_scmi_notify { + SCMI_DEV_PROBE = 0, + SCMI_DEV_STATE, + SCMI_COM_IDLE, + SCMI_DEV_REQUEST, + SCMI_DEV_ADANDON, + SCMI_NOTIFY_NUMBER, +}; + +enum mtk_pwrap_scmi_data { + SCMI_DATA_00 = 0, + SCMI_DATA_01, + SCMI_DATA_02, + SCMI_DATA_03, + SCMI_DATA_NUMBER, +}; + +struct pwrap_scmi_config { + u32 feat_id; + u32 dev_id; + u32 data[SCMI_NOTIFY_NUMBER][SCMI_DATA_NUMBER]; +}; + +#endif diff --git a/drivers/soc/mediatek/power_wrap/power_wrap.c b/drivers/soc/mediatek/power_wrap/power_wrap.c new file mode 100644 index 0000000000000..d2f85dcdbfe3b --- /dev/null +++ b/drivers/soc/mediatek/power_wrap/power_wrap.c @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2026 MediaTek Inc. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "power_wrap.h" + +/* + * ACPI _DSM UUID for DPM + * (64B66B51-38F7-3391-C689-4FA2109179AB) + */ +const guid_t pwrap_dsm_dpm_uuid = GUID_INIT( + 0x64b66b51, 0x38f7, 0x3391, + 0xc6, 0x89, 0x4f, 0xa2, 0x10, 0x91, 0x79, 0xab +); + +enum mtk_pwrap_type { + NVDA6210, +}; + +enum acpi_crs_mem_res { + /* ACPI memory resource data type */ + ACPI_CRS_MEM_RES0 = 0, + ACPI_CRS_MEM_RES1, + ACPI_CRS_MEM_RES_NUM, +}; + +enum acpi_crs_irq_res { + /* ACPI interrupt resource data type */ + ACPI_CRS_IRQ_RES0 = 0, + ACPI_CRS_IRQ_RES1, + ACPI_CRS_IRQ_RES_NUM, +}; + +static struct pwrap_dev_ctrl dev_ctrl; + +/* Getter for static pwrap_dev_ctrl */ +struct pwrap_dev_ctrl *pwrap_get_dev_ctrl(void) +{ + return &dev_ctrl; +} + +/** + * Query dev_config by device path. + * Return pointer to struct pwrap_dev_config as void*, NULL otherwise. + */ +void *pwrap_query_dev_config(const char *dev_path) +{ + struct pwrap_dev_ctrl *ctrl = pwrap_get_dev_ctrl(); + + if (!dev_path) + return NULL; + + //pr_info("%s: search dev_path \"%s\"\n", + // __func__, dev_path); + + if (!ctrl || !ctrl->configs) + return NULL; + + for (int i = 0; i < ctrl->count; ++i) { + char *cfg_path = ctrl->configs[i].device_path; + //pr_info("%s: device configs[%d] path %s\n", + // __func__, i, ctrl->configs[i].device_path); + if (cfg_path && strcmp(dev_path, cfg_path) == 0) + return (void *) &ctrl->configs[i]; + } + return NULL; +} + +/* + * Common SCMI dispatch helper. + * Logs the raw payload, then calls sspm_ci_set() with the provided composite + * word as DATA_00 and the remaining three data words from the action slot. + * Returns the sspm_ci_set() return value, or -EINVAL if control_type is not + * CTRL_BY_SCMI. + */ +static int pwrap_scmi_dispatch(struct pwrap_dev_config *cfg, + u8 action_idx, u32 composite, + const char *action_name) +{ + if (cfg->control_type != CTRL_BY_SCMI) { + pr_warn("%s: control type 0x%x is not scmi for dev %s\n", + action_name, cfg->control_type, cfg->device_path); + return -EINVAL; + } + + pr_debug("%s: scmi payload: 0x%x, 0x%x, 0x%x, 0x%x, 0x%x, 0x%x\n", + action_name, + cfg->scmi_config.feat_id, + cfg->scmi_config.dev_id, + composite, + cfg->scmi_config.data[action_idx][SCMI_DATA_01], + cfg->scmi_config.data[action_idx][SCMI_DATA_02], + cfg->scmi_config.data[action_idx][SCMI_DATA_03]); + + return sspm_ci_set(cfg->scmi_config.feat_id, + cfg->scmi_config.dev_id, + composite, + cfg->scmi_config.data[action_idx][SCMI_DATA_01], + cfg->scmi_config.data[action_idx][SCMI_DATA_02], + cfg->scmi_config.data[action_idx][SCMI_DATA_03]); +} + +int mtk_pwrap_dev_request(void *dev_cfg) +{ + struct pwrap_dev_config *cfg = (struct pwrap_dev_config *)dev_cfg; + + if (!cfg) + return -ENODEV; + return pwrap_scmi_dispatch(cfg, SCMI_DEV_REQUEST, + cfg->scmi_config.data[SCMI_DEV_REQUEST][SCMI_DATA_00], + __func__); +} +EXPORT_SYMBOL_GPL(mtk_pwrap_dev_request); + +int mtk_pwrap_dev_remove(void *dev_cfg) +{ + struct pwrap_dev_config *cfg = (struct pwrap_dev_config *)dev_cfg; + + if (!cfg) + return -ENODEV; + return pwrap_scmi_dispatch(cfg, SCMI_DEV_ADANDON, + cfg->scmi_config.data[SCMI_DEV_ADANDON][SCMI_DATA_00], + __func__); +} +EXPORT_SYMBOL_GPL(mtk_pwrap_dev_remove); + +int mtk_pwrap_dev_suspend(void *dev_cfg, u8 sys_trans) +{ + struct pwrap_dev_config *cfg = (struct pwrap_dev_config *)dev_cfg; + u32 composite; + + if (!cfg) + return -ENODEV; + composite = PWRAP_SCMI_DEV_ACT_PAYLOAD( + cfg->scmi_config.data[SCMI_DEV_STATE][SCMI_DATA_00], DEV_STA_D3, 1, sys_trans); + return pwrap_scmi_dispatch(cfg, SCMI_DEV_STATE, composite, __func__); +} +EXPORT_SYMBOL_GPL(mtk_pwrap_dev_suspend); + +int mtk_pwrap_dev_resume(void *dev_cfg, u8 sys_trans) +{ + struct pwrap_dev_config *cfg = (struct pwrap_dev_config *)dev_cfg; + u32 composite; + + if (!cfg) + return -ENODEV; + composite = PWRAP_SCMI_DEV_ACT_PAYLOAD( + cfg->scmi_config.data[SCMI_DEV_STATE][SCMI_DATA_00], DEV_STA_D0, 0, sys_trans); + return pwrap_scmi_dispatch(cfg, SCMI_DEV_STATE, composite, __func__); +} +EXPORT_SYMBOL_GPL(mtk_pwrap_dev_resume); + +int mtk_pwrap_com_idle(void *dev_cfg, u8 com_idx) +{ + struct pwrap_dev_config *cfg = (struct pwrap_dev_config *)dev_cfg; + u32 composite; + + if (!cfg) + return -ENODEV; + composite = PWRAP_SCMI_DEV_ACT_PAYLOAD( + cfg->scmi_config.data[SCMI_COM_IDLE][SCMI_DATA_00], com_idx, COM_STA_L1, 1); + return pwrap_scmi_dispatch(cfg, SCMI_COM_IDLE, composite, __func__); +} +EXPORT_SYMBOL_GPL(mtk_pwrap_com_idle); + +int mtk_pwrap_com_active(void *dev_cfg, u8 com_idx) +{ + struct pwrap_dev_config *cfg = (struct pwrap_dev_config *)dev_cfg; + u32 composite; + + if (!cfg) + return -ENODEV; + composite = PWRAP_SCMI_DEV_ACT_PAYLOAD( + cfg->scmi_config.data[SCMI_COM_IDLE][SCMI_DATA_00], com_idx, COM_STA_L0, 0); + return pwrap_scmi_dispatch(cfg, SCMI_COM_IDLE, composite, __func__); +} +EXPORT_SYMBOL_GPL(mtk_pwrap_com_active); + +/** + * Probe dev_config by ACPI path, with SCMI log if needed. + * Return pointer to struct pwrap_dev_config as void*, NULL otherwise. + */ +void *mtk_pwrap_dev_probe(const char *acpi_path) +{ + int ret = 0; + struct pwrap_dev_config *cfg = + (struct pwrap_dev_config *) pwrap_query_dev_config(acpi_path); + + if (!cfg) + return NULL; + + pr_info("%s: Device found at path %s\n", __func__, cfg->device_path); + + if (cfg->control_type == CTRL_BY_SCMI) { + pr_info("%s: control type is scmi, payload: 0x%x, 0x%x, 0x%x, 0x%x, 0x%x, 0x%x\n", + __func__, + cfg->scmi_config.feat_id, + cfg->scmi_config.dev_id, + cfg->scmi_config.data[SCMI_DEV_PROBE][SCMI_DATA_00], + cfg->scmi_config.data[SCMI_DEV_PROBE][SCMI_DATA_01], + cfg->scmi_config.data[SCMI_DEV_PROBE][SCMI_DATA_02], + cfg->scmi_config.data[SCMI_DEV_PROBE][SCMI_DATA_03]); + + ret = sspm_ci_set( + cfg->scmi_config.feat_id, + cfg->scmi_config.dev_id, + cfg->scmi_config.data[SCMI_DEV_PROBE][SCMI_DATA_00], + cfg->scmi_config.data[SCMI_DEV_PROBE][SCMI_DATA_01], + cfg->scmi_config.data[SCMI_DEV_PROBE][SCMI_DATA_02], + cfg->scmi_config.data[SCMI_DEV_PROBE][SCMI_DATA_03]); + + if (ret) { + pr_warn("%s: scmi ret = %d for device \"%s\"\n", + __func__, ret, acpi_path); + cfg = NULL; + } + } + return (void *) cfg; +} +EXPORT_SYMBOL_GPL(mtk_pwrap_dev_probe); + +static int pwrap_config_resource(struct platform_device *pdev, kernel_ulong_t driver_data) +{ + enum mtk_pwrap_type type; + struct resource *res; + int irq; + + if (!pdev) + return -ENODEV; + + type = (enum mtk_pwrap_type)driver_data; + switch (type) { + case NVDA6210: + res = platform_get_resource(pdev, IORESOURCE_MEM, ACPI_CRS_MEM_RES0); + if (!res) { + dev_err(&pdev->dev, "no memory resource for index: 0x%x\n", + ACPI_CRS_MEM_RES0); + return -ENOMEM; + } + dev_info(&pdev->dev, "ACPI MEM index 0x%x: start = 0x%llx, size = 0x%llx\n", + ACPI_CRS_MEM_RES0, + (unsigned long long)res->start, + (unsigned long long)resource_size(res)); + + irq = platform_get_irq(pdev, ACPI_CRS_IRQ_RES0); + if (irq < 0) { + dev_err(&pdev->dev, "no IRQ resource for index: 0x%x, error: %d\n", + ACPI_CRS_IRQ_RES0, irq); + return irq; + } + dev_info(&pdev->dev, "ACPI IRQ index 0x%x: number = %d\n", + ACPI_CRS_IRQ_RES0, irq); + break; + default: + dev_err(&pdev->dev, "not support memory resource for type: 0x%x\n", type); + return -EINVAL; + } + + return 0; +} + +static int pwrap_acpi_config_init(struct device *dev) +{ + struct pwrap_dev_ctrl *ctrl = pwrap_get_dev_ctrl(); + int err; + + if (!dev) + return -ENODEV; + + if (!ctrl) + return -EINVAL; + + dev_info(dev, "Config dev ctrl info from ACPI data\n"); + + /* + * Parse the full DPMT (UUID 64B66B51, func 1) into ctrl: device path, + * type, constraints, control_type, and - for SCMI devices - the + * per-device dev_id via GDSC(). Implemented in acpiutil.c. + */ + err = pwrap_acpi_fetch_dpm_config(dev, ctrl); + if (err) { + dev_err(dev, "Failed to fetch DPM config from ACPI: %d\n", err); + memset(ctrl, 0, sizeof(struct pwrap_dev_ctrl)); + } + + return err; +} + +/** + * pwrap_probe_init_resources() - Set up drvdata, ACPI path, and device configs + * @priv: driver private data (pdev/dev already set by caller) + * @adev: ACPI companion device + * @id: matched ACPI device id (carries driver_data for resource config) + * + * Resolves the ACPI device path, configures the platform resources (MEM/IRQ), + * initializes the device control config from either ACPI _DSM or the static + * table, and then runs the initial SCMI device probe against the populated + * config table. The device config init is non-fatal (logged as a warning); + * ACPI path resolution and resource config failures are fatal. + * + * Return: 0 on success, negative errno on a fatal resource/ACPI failure. + */ +static void pwrap_free_acpi_path(void *pointer) +{ + ACPI_FREE(pointer); +} + +static int pwrap_probe_init_resources(struct pwrap_driver_data *priv, + struct acpi_device *adev, + const struct acpi_device_id *id) +{ + struct platform_device *pdev = priv->pdev; + struct device *dev = priv->dev; + acpi_status status; + int ret; + + priv->acpi_path.length = ACPI_ALLOCATE_BUFFER; + priv->acpi_path.pointer = NULL; + status = acpi_get_name(adev->handle, ACPI_FULL_PATHNAME, &priv->acpi_path); + if (ACPI_FAILURE(status)) { + dev_err(&pdev->dev, "Failed to get ACPI device path\n"); + return -EINVAL; + } + ret = devm_add_action_or_reset(dev, pwrap_free_acpi_path, + priv->acpi_path.pointer); + if (ret) + return ret; + + ret = pwrap_config_resource(pdev, id->driver_data); + if (ret) { + dev_err(&pdev->dev, "failed to setup resource for type: 0x%x with error: 0x%x\n", + (enum mtk_pwrap_type)id->driver_data, ret); + return ret; + } + + ret = pwrap_acpi_config_init(dev); + if (ret) + dev_warn(&pdev->dev, + "Device config init failed: %d (SCMI device control disabled)\n", + ret); + + /* + * Run the initial SCMI device probe only after the device-config table + * has been populated by pwrap_acpi_config_init(); otherwise the lookup + * walks an empty table and the boot-time SCMI probe is silently skipped. + */ + if (!mtk_pwrap_dev_probe((char *)priv->acpi_path.pointer)) + dev_err(&pdev->dev, "Not find device config data\n"); + + return 0; +} + +static int mtk_pwrap_probe(struct platform_device *pdev) +{ + const struct acpi_device_id *id; + struct device *dev = &pdev->dev; + struct acpi_device *adev = ACPI_COMPANION(dev); + int ret; + struct pwrap_driver_data *priv; + + if (!adev) + return -ENODEV; + + id = acpi_match_device(dev->driver->acpi_match_table, dev); + if (!id) + return -ENODEV; + + dev_info(&pdev->dev, "device probe for \"%s\"\n", id->id); + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + platform_set_drvdata(pdev, priv); + priv->pdev = pdev; + priv->dev = dev; + + /* Resolve ACPI path, configure resources, init device configs */ + ret = pwrap_probe_init_resources(priv, adev, id); + if (ret) + return ret; + + /* + * The sysfs nodes are debug/test hooks. Their creation must not fail + * probe or disable the driver, so log a warning and continue if the + * attribute group could not be added. + */ + ret = pwrap_create_sys_files(pdev); + if (ret) + dev_warn(&pdev->dev, + "failed to create sysfs nodes: %d (continuing)\n", ret); + + return 0; +} + +#ifdef CONFIG_ACPI +static const struct acpi_device_id mtk_pwrap_acpi_ids[] = { + { .id = "NVDA6210", .driver_data = NVDA6210 }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(acpi, mtk_pwrap_acpi_ids); +#endif /* CONFIG_ACPI */ + +static struct platform_driver mtk_pwrap_driver = { + .driver = { + .name = "mtk_pwrap", +#ifdef CONFIG_ACPI + .acpi_match_table = mtk_pwrap_acpi_ids, +#endif /* CONFIG_ACPI */ + /* + * mtk_pwrap_dev_probe() hands consumers raw pointers into the + * dev_ctrl configs[] table. The driver is built in, the ACPI + * device is never hot-removed and there is no remove callback, + * so the table lives for the lifetime of the kernel and those + * pointers stay valid. Suppress the sysfs bind/unbind + * attributes so userspace cannot rebind the driver either. + */ + .suppress_bind_attrs = true, + }, + .probe = mtk_pwrap_probe, +}; + +static int __init power_wrap_init(void) +{ + int ret = 0; + + ret = platform_driver_register(&mtk_pwrap_driver); + + pr_info("%s driver register: %d\n", __func__, ret); + + return ret; +} + +/* + * This driver is built-in (obj-y) and bound for the lifetime of the system; + * it provides no module_exit()/remove path beyond the platform .remove above. + */ +subsys_initcall(power_wrap_init); +MODULE_LICENSE("GPL"); diff --git a/include/linux/soc/mediatek/mtk-pwrap.h b/include/linux/soc/mediatek/mtk-pwrap.h new file mode 100644 index 0000000000000..e884a461a7ee4 --- /dev/null +++ b/include/linux/soc/mediatek/mtk-pwrap.h @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2026 MediaTek Inc. + */ +#ifndef __MTK_PWRAP_PUBLIC_H__ +#define __MTK_PWRAP_PUBLIC_H__ + +#include +#include + +enum mtk_pwrap_dev_state { + DEV_STA_UNKNOWN = 0, + DEV_STA_D0, + DEV_STA_D1, + DEV_STA_D2, + DEV_STA_D3, + DEV_STA_MAX +}; + +enum mtk_pwrap_com_state { + COM_STA_L0 = 0, + COM_STA_L1, + COM_STA_L2, + COM_STA_L3, + COM_STA_MAX +}; + +#ifdef CONFIG_MTK_POWER_WRAP +int mtk_pwrap_dev_request(void *dev_ctrl); +int mtk_pwrap_dev_remove(void *dev_ctrl); +int mtk_pwrap_dev_suspend(void *dev_ctrl, u8 sys_trans); +int mtk_pwrap_dev_resume(void *dev_ctrl, u8 sys_trans); +int mtk_pwrap_com_idle(void *dev_ctrl, u8 com_idx); +int mtk_pwrap_com_active(void *dev_ctrl, u8 com_idx); +void *mtk_pwrap_dev_probe(const char *acpi_path); +#else +static inline int mtk_pwrap_dev_request(void *dev_ctrl) +{ + return -ENODEV; +} + +static inline int mtk_pwrap_dev_remove(void *dev_ctrl) +{ + return -ENODEV; +} + +static inline int mtk_pwrap_dev_suspend(void *dev_ctrl, u8 sys_trans) +{ + return -ENODEV; +} + +static inline int mtk_pwrap_dev_resume(void *dev_ctrl, u8 sys_trans) +{ + return -ENODEV; +} + +static inline int mtk_pwrap_com_idle(void *dev_ctrl, u8 com_idx) +{ + return -ENODEV; +} + +static inline int mtk_pwrap_com_active(void *dev_ctrl, u8 com_idx) +{ + return -ENODEV; +} + +static inline void *mtk_pwrap_dev_probe(const char *acpi_path) +{ + return NULL; +} +#endif /* CONFIG_MTK_POWER_WRAP */ + +#endif From 65f36ae175ca2a3aeb264e0a742de2ea4e0ab4f5 Mon Sep 17 00:00:00 2001 From: "mayuresh.wani" Date: Thu, 3 Sep 2026 22:49:31 -0700 Subject: [PATCH 3/9] NVIDIA: SAUCE: soc: mediatek: power_wrap: add sysfs debug interface Add a sysfs interface to the power_wrap driver for inspecting and exercising device power control requests from userspace. This is a debug and validation aid; the functional path is the in-kernel client API and the ACPI flows. Signed-off-by: mayuresh.wani Signed-off-by: David Cemin --- drivers/soc/mediatek/power_wrap/Makefile | 1 + .../mediatek/power_wrap/power_wrap_sysfs.c | 420 ++++++++++++++++++ 2 files changed, 421 insertions(+) create mode 100644 drivers/soc/mediatek/power_wrap/power_wrap_sysfs.c diff --git a/drivers/soc/mediatek/power_wrap/Makefile b/drivers/soc/mediatek/power_wrap/Makefile index 3a551b55b31c7..0765daf225261 100644 --- a/drivers/soc/mediatek/power_wrap/Makefile +++ b/drivers/soc/mediatek/power_wrap/Makefile @@ -3,6 +3,7 @@ # Composite name power_wrap_drv avoids colliding with the source file power_wrap.o obj-y += power_wrap_drv.o power_wrap_drv-y += power_wrap.o +power_wrap_drv-y += power_wrap_sysfs.o power_wrap_drv-y += acpiutil.o ccflags-y += -Wall diff --git a/drivers/soc/mediatek/power_wrap/power_wrap_sysfs.c b/drivers/soc/mediatek/power_wrap/power_wrap_sysfs.c new file mode 100644 index 0000000000000..f6ee5634621c3 --- /dev/null +++ b/drivers/soc/mediatek/power_wrap/power_wrap_sysfs.c @@ -0,0 +1,420 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2026 MediaTek Inc. + * + * power_wrap_sysfs.c - sysfs interface for the power_wrap driver + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "power_wrap.h" + +#define SYSFS_BUF_SIZE 128 + +/* + * search_path caches the last path written via pwrap_dev_config_query_store() + * for the paired _show() to look up. It is shared state, so serialize all + * store/show access under search_path_lock to prevent a concurrent writer from + * tearing the string a reader is parsing. + */ +static char search_path[SYSFS_BUF_SIZE]; +static DEFINE_MUTEX(search_path_lock); + +/** + * pwrap_sysfs_get_cfg() - Parse an ACPI path from a sysfs write and look it up + * @buf: sysfs write buffer (NOT NUL-terminated; @count valid bytes) + * @count: number of bytes written + * @path: caller-provided output buffer of size SYSFS_BUF_SIZE + * @out_cfg: receives the matching struct pwrap_dev_config pointer on success + * + * Copies @count bytes into @path, NUL-terminates, strips a trailing newline, + * logs the parsed path, and looks up the device config via + * pwrap_query_dev_config(). + * + * Return: 0 and sets *@out_cfg on success, -EINVAL if the input exceeds the + * buffer, -ENODEV if no device config matches the path. + */ +static int pwrap_sysfs_get_cfg(const char *buf, size_t count, char *path, + void **out_cfg) +{ + char norm[SYSFS_BUF_SIZE]; + + if (count >= SYSFS_BUF_SIZE) + return -EINVAL; + + memcpy(path, buf, count); + path[count] = '\0'; + if (count > 0 && path[count - 1] == '\n') + path[count - 1] = '\0'; + + pr_info("power_wrap sysfs: user input acpi device path = %s\n", path); + + /* + * Device configs are stored under padded ACPI paths (\_SB_.USB0); + * accept the unpadded spelling users type (\_SB.USB0) too. + */ + if (pwrap_acpi_normalize_path(path, norm, sizeof(norm))) + return -EINVAL; + + *out_cfg = pwrap_query_dev_config(norm); + if (!*out_cfg) { + pr_warn("power_wrap sysfs: no device config for path %s\n", path); + return -ENODEV; + } + + return 0; +} + +static ssize_t pwrap_dev_probe_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int len = 0; + struct pwrap_dev_ctrl *ctrl = pwrap_get_dev_ctrl(); + + if (!ctrl) { + len += scnprintf(buf + len, PAGE_SIZE - len, + "pwrap_dev_ctrl not available\n"); + return len; + } + + len += scnprintf(buf + len, PAGE_SIZE - len, + "revision = %llu\ncount = %llu\n", ctrl->revision, ctrl->count); + + if (ctrl->count > 0 && ctrl->configs) { + len += scnprintf(buf + len, PAGE_SIZE - len, + "Device List:\n"); + for (int i = 0; i < ctrl->count && len < PAGE_SIZE - 1; ++i) { + const char *path = ctrl->configs[i].device_path ? + ctrl->configs[i].device_path : "(null)"; + int type = ctrl->configs[i].control_type; + + len += scnprintf(buf + len, PAGE_SIZE - len, + " [%d] device_path = %s, control_type = %d", i, path, type); + switch (type) { + case CTRL_NONE: + len += scnprintf(buf + len, PAGE_SIZE - len, " (CTRL_NONE)"); + break; + case CTRL_BY_PWRAP: + len += scnprintf(buf + len, PAGE_SIZE - len, " (CTRL_BY_PWRAP)"); + break; + case CTRL_BY_SCMI: + len += scnprintf(buf + len, PAGE_SIZE - len, " (CTRL_BY_SCMI)"); + break; + default: + len += scnprintf(buf + len, PAGE_SIZE - len, " (UNKNOWN)"); + break; + } + len += scnprintf(buf + len, PAGE_SIZE - len, "\n"); + if (len >= PAGE_SIZE - 1) { + len += scnprintf(buf + len, PAGE_SIZE - len, "[truncated]\n"); + break; + } + } + } + + return len; +} + +static ssize_t pwrap_dev_probe_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + char acpi_path[SYSFS_BUF_SIZE]; + void *dev_cfg; + + if (count >= SYSFS_BUF_SIZE) + return -EINVAL; + + memcpy(acpi_path, buf, count); + acpi_path[count] = '\0'; + if (count > 0 && acpi_path[count - 1] == '\n') + acpi_path[count - 1] = '\0'; + + pr_info("power_wrap sysfs: user input acpi device path = %s\n", acpi_path); + + dev_cfg = mtk_pwrap_dev_probe(acpi_path); + if (!dev_cfg) { + pr_warn("power_wrap sysfs: device probe failed for path %s\n", + acpi_path); + return -ENODEV; + } + + return count; +} +DEVICE_ATTR_RW(pwrap_dev_probe); + +static ssize_t pwrap_dev_remove_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + char acpi_path[SYSFS_BUF_SIZE]; + void *dev_cfg; + int err; + + err = pwrap_sysfs_get_cfg(buf, count, acpi_path, &dev_cfg); + if (err) + return err; + + err = mtk_pwrap_dev_remove(dev_cfg); + if (err) + return err; + + return count; +} +DEVICE_ATTR_WO(pwrap_dev_remove); + +static ssize_t pwrap_dev_request_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + char acpi_path[SYSFS_BUF_SIZE]; + void *dev_cfg; + int err; + + err = pwrap_sysfs_get_cfg(buf, count, acpi_path, &dev_cfg); + if (err) + return err; + + err = mtk_pwrap_dev_request(dev_cfg); + if (err) + return err; + + return count; +} +DEVICE_ATTR_WO(pwrap_dev_request); + +static ssize_t pwrap_dev_suspend_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + char acpi_path[SYSFS_BUF_SIZE]; + void *dev_cfg; + int err; + + err = pwrap_sysfs_get_cfg(buf, count, acpi_path, &dev_cfg); + if (err) + return err; + + err = mtk_pwrap_dev_suspend(dev_cfg, 0); + if (err) + return err; + + return count; +} +DEVICE_ATTR_WO(pwrap_dev_suspend); + +static ssize_t pwrap_dev_resume_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + char acpi_path[SYSFS_BUF_SIZE]; + void *dev_cfg; + int err; + + err = pwrap_sysfs_get_cfg(buf, count, acpi_path, &dev_cfg); + if (err) + return err; + + err = mtk_pwrap_dev_resume(dev_cfg, 0); + if (err) + return err; + + return count; +} +DEVICE_ATTR_WO(pwrap_dev_resume); + +static ssize_t pwrap_com_idle_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + char acpi_path[SYSFS_BUF_SIZE]; + void *dev_cfg; + int err; + + err = pwrap_sysfs_get_cfg(buf, count, acpi_path, &dev_cfg); + if (err) + return err; + + err = mtk_pwrap_com_idle(dev_cfg, 0); + if (err) + return err; + + return count; +} +DEVICE_ATTR_WO(pwrap_com_idle); + +static ssize_t pwrap_com_active_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + char acpi_path[SYSFS_BUF_SIZE]; + void *dev_cfg; + int err; + + err = pwrap_sysfs_get_cfg(buf, count, acpi_path, &dev_cfg); + if (err) + return err; + + err = mtk_pwrap_com_active(dev_cfg, 0); + if (err) + return err; + + return count; +} +DEVICE_ATTR_WO(pwrap_com_active); + +static ssize_t pwrap_dev_config_query_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct pwrap_dev_config *dev_cfg; + int len = 0; + int i, j; + + mutex_lock(&search_path_lock); + + if (search_path[0] == '\0') { + len += scnprintf(buf + len, PAGE_SIZE - len, + "No device path set for search\n"); + mutex_unlock(&search_path_lock); + return len; + } + + dev_cfg = (struct pwrap_dev_config *) pwrap_query_dev_config(search_path); + mutex_unlock(&search_path_lock); + if (!dev_cfg) + return -ENODEV; + + len += scnprintf(buf + len, PAGE_SIZE - len, + "device_path = %s\n", dev_cfg->device_path ? dev_cfg->device_path : "(null)"); + len += scnprintf(buf + len, PAGE_SIZE - len, + "device_type = %s\n", dev_cfg->device_type ? dev_cfg->device_type : "(null)"); + + // constraints + len += scnprintf(buf + len, PAGE_SIZE - len, + "constraints:\n"); + len += scnprintf(buf + len, PAGE_SIZE - len, + " enable = %llu\n", dev_cfg->constraints.enable); + len += scnprintf(buf + len, PAGE_SIZE - len, + " device_state = %llu\n", dev_cfg->constraints.device_state); + len += scnprintf(buf + len, PAGE_SIZE - len, + " comp_count = %llu\n", dev_cfg->constraints.comp_count); + len += scnprintf(buf + len, PAGE_SIZE - len, + " comp_states = ["); + for (i = 0; i < dev_cfg->constraints.comp_count; i++) { + len += scnprintf(buf + len, PAGE_SIZE - len, + "%llu%s", dev_cfg->constraints.comp_states[i], + (i < dev_cfg->constraints.comp_count - 1) ? ", " : ""); + } + len += scnprintf(buf + len, PAGE_SIZE - len, "]\n"); + + // control_type + len += scnprintf(buf + len, PAGE_SIZE - len, + "control_type = %d", dev_cfg->control_type); + switch (dev_cfg->control_type) { + case CTRL_NONE: + len += scnprintf(buf + len, PAGE_SIZE - len, " (CTRL_NONE)\n"); + break; + case CTRL_BY_PWRAP: + len += scnprintf(buf + len, PAGE_SIZE - len, " (CTRL_BY_PWRAP)\n"); + break; + case CTRL_BY_SCMI: + len += scnprintf(buf + len, PAGE_SIZE - len, " (CTRL_BY_SCMI)\n"); + break; + default: + len += scnprintf(buf + len, PAGE_SIZE - len, " (UNKNOWN)\n"); + break; + } + + // scmi_config + len += scnprintf(buf + len, PAGE_SIZE - len, + "scmi_config:\n"); + len += scnprintf(buf + len, PAGE_SIZE - len, + " feat_id = 0x%x\n", dev_cfg->scmi_config.feat_id); + len += scnprintf(buf + len, PAGE_SIZE - len, + " dev_id = 0x%x\n", dev_cfg->scmi_config.dev_id); + len += scnprintf(buf + len, PAGE_SIZE - len, + " data = [\n"); + for (i = 0; i < SCMI_NOTIFY_NUMBER; i++) { + len += scnprintf(buf + len, PAGE_SIZE - len, " ["); + for (j = 0; j < SCMI_DATA_NUMBER; j++) { + len += scnprintf(buf + len, PAGE_SIZE - len, + "0x%x%s", dev_cfg->scmi_config.data[i][j], + (j < SCMI_DATA_NUMBER - 1) ? ", " : ""); + } + len += scnprintf(buf + len, PAGE_SIZE - len, "]\n"); + } + len += scnprintf(buf + len, PAGE_SIZE - len, " ]\n"); + + return len; +} + +static ssize_t pwrap_dev_config_query_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + if (count >= SYSFS_BUF_SIZE) + return -EINVAL; + + mutex_lock(&search_path_lock); + + memset(search_path, 0, sizeof(search_path)); + memcpy(search_path, buf, count); + search_path[count] = '\0'; + if (count > 0 && search_path[count - 1] == '\n') + search_path[count - 1] = '\0'; + + pr_info("power_wrap sysfs: user input acpi device path = %s\n", + search_path); + + mutex_unlock(&search_path_lock); + + return count; +} +DEVICE_ATTR_RW(pwrap_dev_config_query); + +static struct attribute *pwrap_attrs[] = { + &dev_attr_pwrap_dev_probe.attr, + &dev_attr_pwrap_dev_remove.attr, + &dev_attr_pwrap_dev_request.attr, + &dev_attr_pwrap_dev_suspend.attr, + &dev_attr_pwrap_dev_resume.attr, + &dev_attr_pwrap_com_idle.attr, + &dev_attr_pwrap_com_active.attr, + &dev_attr_pwrap_dev_config_query.attr, + NULL, +}; + +static const struct attribute_group pwrap_group = { + .attrs = pwrap_attrs, +}; + +/** + * pwrap_create_sys_files() - Create the sysfs debug/test nodes + * @pdev: the power_wrap platform device + * + * Registers all nodes as one atomic attribute group: creation is + * all-or-nothing, so there is never a partial sysfs ABI. The group is + * devm-managed and removed automatically on unbind. + * + * These are debug/test nodes, so a creation failure must NOT fail probe or + * disable the driver - the SCMI device-control path stays functional either + * way. The caller logs and ignores the return value. + * + * Return: 0 on success, negative errno if the group could not be added. + */ +int pwrap_create_sys_files(struct platform_device *pdev) +{ + if (!pdev) + return -EINVAL; + + return devm_device_add_group(&pdev->dev, &pwrap_group); +} From e3f61a395b1565367f27d5fbe08a3af9b49f9020 Mon Sep 17 00:00:00 2001 From: "mayuresh.wani" Date: Thu, 3 Sep 2026 22:49:31 -0700 Subject: [PATCH 4/9] NVIDIA: SAUCE: soc: mediatek: power_wrap: add client power control API Add mtk_send_power_control_req(), the in-kernel client entry point for pushing a power control command to the SSPM. Kernel drivers that manage SSPM-controlled resources (for example the audio clock tree and the audio DSP) fill a request descriptor and submit it through this interface instead of open coding the SCMI exchange. Signed-off-by: mayuresh.wani Signed-off-by: Kaushal Rajeev Butala Signed-off-by: David Cemin --- drivers/soc/mediatek/power_wrap/power_wrap.c | 94 ++++++++++++++++++++ include/linux/soc/mediatek/mtk-pwrap.h | 48 ++++++++++ 2 files changed, 142 insertions(+) diff --git a/drivers/soc/mediatek/power_wrap/power_wrap.c b/drivers/soc/mediatek/power_wrap/power_wrap.c index d2f85dcdbfe3b..e1505fcb5e429 100644 --- a/drivers/soc/mediatek/power_wrap/power_wrap.c +++ b/drivers/soc/mediatek/power_wrap/power_wrap.c @@ -121,6 +121,100 @@ int mtk_pwrap_dev_request(void *dev_cfg) } EXPORT_SYMBOL_GPL(mtk_pwrap_dev_request); +/** + * mtk_send_power_control_req() - Send a client power control request to SSPM + * @pwrctlreq: caller-allocated and -filled request descriptor + * + * Entry point for other drivers that need to push a power control command to + * SSPM over the SCMI interface. The caller allocates and fills @pwrctlreq + * (including @dev and an @InBuffer pointing to a struct power_cntl_scmi_data). + * + * The flow is: + * 1. Validate @pwrctlreq and its buffers. + * 2. Resolve @dev's ACPI full pathname and use it to look up the matching + * struct pwrap_dev_config (which carries the per-device scmi_config with + * feat_id/dev_id and the SCMI_DEV_REQUEST data words). + * 3. Interpret @InBuffer as struct power_cntl_scmi_data. + * 4. OR the caller's requestId/customdata into the configured request data + * words and issue sspm_ci_set(). + * + * Return: 0 on success, negative errno on failure. + */ +int mtk_send_power_control_req(struct _power_cntrl_request *pwrctlreq) +{ + struct power_cntl_scmi_data *scmidata; + struct pwrap_scmi_config *scmicfg; + struct pwrap_dev_config *cfg; + struct acpi_device *adev; + struct acpi_buffer path = { ACPI_ALLOCATE_BUFFER, NULL }; + acpi_status status; + int ret; + + if (!pwrctlreq || !pwrctlreq->dev) + return -EINVAL; + + if (!pwrctlreq->InBuffer || + pwrctlreq->InBufferSize < sizeof(struct power_cntl_scmi_data)) { + dev_err(pwrctlreq->dev, "%s: invalid input buffer\n", __func__); + return -EINVAL; + } + + /* Extract the ACPI companion and its full pathname from dev. */ + adev = ACPI_COMPANION(pwrctlreq->dev); + if (!adev) { + dev_err(pwrctlreq->dev, "%s: no ACPI companion\n", __func__); + return -ENODEV; + } + + status = acpi_get_name(adev->handle, ACPI_FULL_PATHNAME, &path); + if (ACPI_FAILURE(status)) { + dev_err(pwrctlreq->dev, "%s: failed to get ACPI path\n", __func__); + return -EINVAL; + } + + /* Use the ACPI path to fetch the per-device config / scmi_config. */ + cfg = pwrap_query_dev_config((char *)path.pointer); + if (!cfg) { + dev_err(pwrctlreq->dev, "%s: no dev config for \"%s\"\n", + __func__, (char *)path.pointer); + ret = -ENODEV; + goto out; + } + + if (cfg->control_type != CTRL_BY_SCMI) { + dev_warn(pwrctlreq->dev, "%s: control type 0x%x is not scmi for dev %s\n", + __func__, cfg->control_type, cfg->device_path); + ret = -EINVAL; + goto out; + } + + scmicfg = &cfg->scmi_config; + + /* Interpret the caller's input buffer as SCMI request payload data. */ + scmidata = (struct power_cntl_scmi_data *)pwrctlreq->InBuffer; + + dev_dbg(pwrctlreq->dev, + "%s: scmi payload: 0x%x, 0x%x, 0x%x, 0x%x, 0x%x, 0x%x\n", __func__, + scmicfg->feat_id, scmicfg->dev_id, + scmicfg->data[SCMI_DEV_REQUEST][SCMI_DATA_00] | scmidata->requestId, + scmicfg->data[SCMI_DEV_REQUEST][SCMI_DATA_01] | scmidata->customdata[SCMI_DATA_00], + scmicfg->data[SCMI_DEV_REQUEST][SCMI_DATA_02] | scmidata->customdata[SCMI_DATA_01], + scmicfg->data[SCMI_DEV_REQUEST][SCMI_DATA_03] | scmidata->customdata[SCMI_DATA_02]); + + ret = sspm_ci_set(scmicfg->feat_id, scmicfg->dev_id, + scmicfg->data[SCMI_DEV_REQUEST][SCMI_DATA_00] | scmidata->requestId, + scmicfg->data[SCMI_DEV_REQUEST][SCMI_DATA_01] | scmidata->customdata[SCMI_DATA_00], + scmicfg->data[SCMI_DEV_REQUEST][SCMI_DATA_02] | scmidata->customdata[SCMI_DATA_01], + scmicfg->data[SCMI_DEV_REQUEST][SCMI_DATA_03] | scmidata->customdata[SCMI_DATA_02]); + + pwrctlreq->BytesReturned = 0; + +out: + ACPI_FREE(path.pointer); + return ret; +} +EXPORT_SYMBOL_GPL(mtk_send_power_control_req); + int mtk_pwrap_dev_remove(void *dev_cfg) { struct pwrap_dev_config *cfg = (struct pwrap_dev_config *)dev_cfg; diff --git a/include/linux/soc/mediatek/mtk-pwrap.h b/include/linux/soc/mediatek/mtk-pwrap.h index e884a461a7ee4..16d98e387c6f2 100644 --- a/include/linux/soc/mediatek/mtk-pwrap.h +++ b/include/linux/soc/mediatek/mtk-pwrap.h @@ -8,6 +8,48 @@ #include #include +struct device; + +/* + * Number of caller-supplied custom data words carried in a power control + * request. These are OR'd into scmi_config.data[SCMI_DEV_REQUEST][SCMI_DATA_01..03] + * before being handed to sspm_ci_set(), so the count matches the three + * trailing data words of the SCMI_DEV_REQUEST action slot. + */ +#define POWER_WRAP_SCMI_DATA_MAX_INDEX 3 + +/* + * Power control request descriptor. + * + * A client driver that wants to send a power control command to SSPM + * allocates and fills one of these, then passes it to + * mtk_send_power_control_req(). @dev identifies the target device (used to + * resolve its ACPI path and, from there, its per-device SCMI config). + * @InBuffer must point to a struct power_cntl_scmi_data. + * + * The Out* / BytesReturned fields mirror the Windows IOCTL-style buffer + * contract this driver is ported from; they are reserved for future use. + */ +struct _power_cntrl_request { + struct device *dev; + void *InBuffer; + size_t InBufferSize; + void *OutBuffer; + size_t OutBufferSize; + size_t BytesReturned; +}; + +/* + * Payload the client places in _power_cntrl_request.InBuffer. + * + * @requestId - OR'd into the SCMI_DATA_00 word (action id) of the request. + * @customdata[] - OR'd into the SCMI_DATA_01..03 words of the request. + */ +struct power_cntl_scmi_data { + u8 requestId; + u32 customdata[POWER_WRAP_SCMI_DATA_MAX_INDEX]; +}; + enum mtk_pwrap_dev_state { DEV_STA_UNKNOWN = 0, DEV_STA_D0, @@ -26,6 +68,7 @@ enum mtk_pwrap_com_state { }; #ifdef CONFIG_MTK_POWER_WRAP +int mtk_send_power_control_req(struct _power_cntrl_request *pwrctlreq); int mtk_pwrap_dev_request(void *dev_ctrl); int mtk_pwrap_dev_remove(void *dev_ctrl); int mtk_pwrap_dev_suspend(void *dev_ctrl, u8 sys_trans); @@ -34,6 +77,11 @@ int mtk_pwrap_com_idle(void *dev_ctrl, u8 com_idx); int mtk_pwrap_com_active(void *dev_ctrl, u8 com_idx); void *mtk_pwrap_dev_probe(const char *acpi_path); #else +static inline int mtk_send_power_control_req(struct _power_cntrl_request *pwrctlreq) +{ + return -ENODEV; +} + static inline int mtk_pwrap_dev_request(void *dev_ctrl) { return -ENODEV; From 56554e813fa12c2b76f401a849ff9f12a4f4e63e Mon Sep 17 00:00:00 2001 From: "mayuresh.wani" Date: Thu, 3 Sep 2026 22:49:31 -0700 Subject: [PATCH 5/9] NVIDIA: SAUCE: soc: mediatek: power_wrap: expose client requests in sysfs Extend the sysfs debug interface to cover the client power control request path, so validation can issue and observe client requests without a kernel consumer. Signed-off-by: mayuresh.wani Signed-off-by: Kaushal Rajeev Butala Signed-off-by: David Cemin --- .../mediatek/power_wrap/power_wrap_sysfs.c | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/drivers/soc/mediatek/power_wrap/power_wrap_sysfs.c b/drivers/soc/mediatek/power_wrap/power_wrap_sysfs.c index f6ee5634621c3..fd7f67ac1a862 100644 --- a/drivers/soc/mediatek/power_wrap/power_wrap_sysfs.c +++ b/drivers/soc/mediatek/power_wrap/power_wrap_sysfs.c @@ -381,6 +381,153 @@ static ssize_t pwrap_dev_config_query_store(struct device *dev, } DEVICE_ATTR_RW(pwrap_dev_config_query); +/* + * pwrap_power_ctrl_req_store - Exercise the mtk_send_power_control_req() client API + * + * Test/debug trigger for the public power control request entry point that + * external consumer drivers (e.g. xhci-mtk-v2) call. It drives the real API + * path end to end: resolve an ACPI path to its acpi_device, find its bound + * physical device, and hand that to mtk_send_power_control_req() (whose + * ACPI_COMPANION() then resolves it back to the ACPI node), which looks up + * the per-device CTRL_BY_SCMI config and issues sspm_ci_set(). This lets the + * client API be validated without needing the consumer driver to suspend. + * + * Input: " [requestId] [customdata0] [customdata1] [customdata2]" + * requestId/customdata are optional hex values (default 0). requestId + * is OR'd into the SCMI_DATA_00 (action) word; customdata into the + * three following words, exactly as a real client request would be. + * + * Usage: + * echo '\_SB.USB0 0x02' > /sys/bus/platform/devices//pwrap_power_ctrl_req + * echo '\_SB.USB0 0x1 0 0 0' > /sys/bus/platform/devices//pwrap_power_ctrl_req + */ +static ssize_t pwrap_power_ctrl_req_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + char kbuf[SYSFS_BUF_SIZE]; + char path[SYSFS_BUF_SIZE]; + u8 reqid = 0; + u32 cdata[POWER_WRAP_SCMI_DATA_MAX_INDEX] = { 0 }; + struct power_cntl_scmi_data scmidata; + struct _power_cntrl_request req; + struct acpi_device *adev; + struct device *phys_dev; + acpi_handle handle; + acpi_status status; + char *p, *tok; + int i, ret; + + if (count >= SYSFS_BUF_SIZE) + return -EINVAL; + + memcpy(kbuf, buf, count); + kbuf[count] = '\0'; + + /* + * Strict tokenized parse of " [reqId] [c0] [c1] [c2]". + * The path is mandatory; reqId and the customdata words are optional + * hex values. Any unparseable field or extra trailing token is + * rejected so malformed input is never silently accepted. reqId is + * parsed with kstrtou8() so a value > 0xff is an error rather than + * being silently truncated to a u8. + */ + p = kbuf; + + /* Mandatory first token: the ACPI path. */ + do { + tok = strsep(&p, " \t\n"); + } while (tok && *tok == '\0'); + if (!tok || *tok == '\0') + return -EINVAL; + if (strscpy(path, tok, sizeof(path)) < 0) + return -EINVAL; + + /* Optional numeric tokens: reqId, then up to 3 customdata words. */ + for (i = 0; i <= POWER_WRAP_SCMI_DATA_MAX_INDEX; i++) { + do { + tok = strsep(&p, " \t\n"); + } while (tok && *tok == '\0'); + if (!tok) + break; + + if (i == 0) { + if (kstrtou8(tok, 16, &reqid)) + return -EINVAL; + } else if (kstrtou32(tok, 16, &cdata[i - 1])) { + return -EINVAL; + } + } + + /* Reject any leftover tokens beyond the expected fields. */ + while (p) { + tok = strsep(&p, " \t\n"); + if (tok && *tok != '\0') + return -EINVAL; + } + + /* Resolve the ACPI path to a device we can hand to the client API. */ + status = acpi_get_handle(NULL, path, &handle); + if (ACPI_FAILURE(status)) { + pr_warn("power_wrap sysfs: cannot resolve ACPI path %s\n", path); + return -ENODEV; + } + + adev = acpi_get_acpi_dev(handle); + if (!adev) { + pr_warn("power_wrap sysfs: no acpi_device for %s\n", path); + return -ENODEV; + } + + /* + * mtk_send_power_control_req() resolves the ACPI companion via + * ACPI_COMPANION(dev), which reads dev->fwnode. An acpi_device's own + * embedded struct device does NOT have its fwnode set back to the ACPI + * node, so &adev->dev would yield a NULL companion. The bound physical + * device (e.g. the xhci-plat platform_device for \_SB.USB0) is the one + * whose dev->fwnode is the ACPI companion — and it's exactly the dev a + * real consumer driver passes. Use it. + * + * acpi_get_first_physical_node() returns a weak device pointer, so take + * a device reference before dropping the acpi_device reference acquired + * by acpi_get_acpi_dev(). + */ + phys_dev = get_device(acpi_get_first_physical_node(adev)); + acpi_dev_put(adev); + + if (!phys_dev) { + pr_warn("power_wrap sysfs: no bound physical device for %s (driver not bound?)\n", + path); + return -ENODEV; + } + + scmidata.requestId = reqid; + scmidata.customdata[0] = cdata[0]; + scmidata.customdata[1] = cdata[1]; + scmidata.customdata[2] = cdata[2]; + + req.dev = phys_dev; + req.InBuffer = &scmidata; + req.InBufferSize = sizeof(scmidata); + req.OutBuffer = NULL; + req.OutBufferSize = 0; + req.BytesReturned = 0; + + pr_info("power_wrap sysfs: power_ctrl_req path=%s reqId=0x%x custom=[0x%x,0x%x,0x%x]\n", + path, reqid, cdata[0], cdata[1], cdata[2]); + + ret = mtk_send_power_control_req(&req); + put_device(phys_dev); + + if (ret) { + pr_warn("power_wrap sysfs: power_ctrl_req failed: %d\n", ret); + return ret; + } + + return count; +} +DEVICE_ATTR_WO(pwrap_power_ctrl_req); + static struct attribute *pwrap_attrs[] = { &dev_attr_pwrap_dev_probe.attr, &dev_attr_pwrap_dev_remove.attr, @@ -390,6 +537,7 @@ static struct attribute *pwrap_attrs[] = { &dev_attr_pwrap_com_idle.attr, &dev_attr_pwrap_com_active.attr, &dev_attr_pwrap_dev_config_query.attr, + &dev_attr_pwrap_power_ctrl_req.attr, NULL, }; From 0f3e20177511bd4b1cd60f39deca8250e68f52b1 Mon Sep 17 00:00:00 2001 From: "mayuresh.wani" Date: Thu, 3 Sep 2026 22:49:32 -0700 Subject: [PATCH 6/9] NVIDIA: SAUCE: soc: mediatek: power_wrap: enable DVFSRC via SPM SYSRAM flag Set the SPM SYSRAM flag that enables DVFSRC so dynamic voltage and frequency scaling resource control is active when the driver has initialized. Signed-off-by: mayuresh.wani Signed-off-by: David Cemin --- .../soc/mediatek/power_wrap/inc/power_wrap.h | 2 + drivers/soc/mediatek/power_wrap/power_wrap.c | 80 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/drivers/soc/mediatek/power_wrap/inc/power_wrap.h b/drivers/soc/mediatek/power_wrap/inc/power_wrap.h index ee0181dab1cb7..a2b64a4c7926f 100644 --- a/drivers/soc/mediatek/power_wrap/inc/power_wrap.h +++ b/drivers/soc/mediatek/power_wrap/inc/power_wrap.h @@ -97,6 +97,8 @@ struct pwrap_driver_data { //struct pwrap_dev_ctrl dev_ctrl; struct platform_device *pdev; struct acpi_buffer acpi_path; + /* Platform id from the PLID method, 0 when unavailable */ + unsigned long long plid; }; /* Create the sysfs debug/test nodes (non-fatal; group is devm-managed) */ diff --git a/drivers/soc/mediatek/power_wrap/power_wrap.c b/drivers/soc/mediatek/power_wrap/power_wrap.c index e1505fcb5e429..660c00992df5b 100644 --- a/drivers/soc/mediatek/power_wrap/power_wrap.c +++ b/drivers/soc/mediatek/power_wrap/power_wrap.c @@ -42,6 +42,29 @@ enum acpi_crs_irq_res { static struct pwrap_dev_ctrl dev_ctrl; +/* + * DVFSRC enable flag in SPM SYSRAM. + * + * The Windows PEP driver (ppmnotify.c, PepPpmLpiSupported()) writes a DVFSRC + * enable flag into SPM SYSRAM so the firmware enables DVFSRC for the + * suspend/resume low-power flow. On Windows the address/length/value are + * seeded into the registry by nvpep.inx (AddReg) and read back at runtime; + * they are fixed platform constants, so we hardcode them here. + * + * Windows source values (nvpep.inx): + * HKR,Parameters\DVFSRC,Offset,%REG_DWORD%,0x12FC00 + * HKR,Parameters\DVFSRC,Length,%REG_DWORD%,0x4 + * HKR,Parameters\DVFSRC,Value, %REG_DWORD%,0x1 + * + * 0x12FC00 is an absolute physical address (not an offset into any base): + * Windows maps it directly via MmMapIoSpaceEx(), so the Linux equivalent is + * ioremap(0x12FC00, 4) + writel(0x1). + */ +#define PWRAP_PLID_MT8901 0x2 /* SOC_MT8901 */ +#define PWRAP_DVFSRC_FLAG_ADDR 0x12FC00 +#define PWRAP_DVFSRC_FLAG_LEN 0x4 +#define PWRAP_DVFSRC_FLAG_VAL 0x1 + /* Getter for static pwrap_dev_ctrl */ struct pwrap_dev_ctrl *pwrap_get_dev_ctrl(void) { @@ -453,6 +476,44 @@ static int pwrap_probe_init_resources(struct pwrap_driver_data *priv, return 0; } +/** + * pwrap_write_dvfsrc_flag() - Enable DVFSRC via the SPM SYSRAM flag + * @dev: device (used for logging) + * + * Writes the DVFSRC enable flag into SPM SYSRAM so the firmware enables + * DVFSRC for the suspend/resume low-power flow. The flag address, length, + * and value are fixed platform constants (see PWRAP_DVFSRC_FLAG_* above). + * + * The flag is written once at probe. This mirrors the initial write done by + * the Windows PEP driver in PepPpmLpiSupported() (ppmnotify.c). SYSRAM + * contents are assumed to persist across the platform sleep states used on + * this target, so no resume-time re-write is performed. + * + * Failure to map the SYSRAM region is non-fatal: the driver continues so the + * SCMI device-control path stays functional. + */ +static void pwrap_write_dvfsrc_flag(struct device *dev) +{ + void __iomem *vaddr; + + vaddr = ioremap(PWRAP_DVFSRC_FLAG_ADDR, PWRAP_DVFSRC_FLAG_LEN); + if (!vaddr) { + dev_warn(dev, + "DVFSRC: failed to map SYSRAM flag at 0x%x (DVFSRC not enabled)\n", + PWRAP_DVFSRC_FLAG_ADDR); + return; + } + + writel(PWRAP_DVFSRC_FLAG_VAL, vaddr); + + dev_info(dev, + "DVFSRC: enabled via SYSRAM flag phys=0x%x len=0x%x val=0x%x\n", + PWRAP_DVFSRC_FLAG_ADDR, PWRAP_DVFSRC_FLAG_LEN, + PWRAP_DVFSRC_FLAG_VAL); + + iounmap(vaddr); +} + static int mtk_pwrap_probe(struct platform_device *pdev) { const struct acpi_device_id *id; @@ -483,6 +544,25 @@ static int mtk_pwrap_probe(struct platform_device *pdev) if (ret) return ret; + /* + * The register writes below are MT8901 platform constants: gate them + * on the PLID platform id so another SoC exposing this HID is left + * untouched. + */ + if (ACPI_FAILURE(acpi_evaluate_integer(adev->handle, "PLID", NULL, + &priv->plid))) + priv->plid = 0; + + /* + * Enable DVFSRC by writing the SPM SYSRAM flag. Ported from the + * Windows PEP driver (PepPpmLpiSupported() in ppmnotify.c). Non-fatal. + */ + if (priv->plid == PWRAP_PLID_MT8901) + pwrap_write_dvfsrc_flag(dev); + else + dev_info(dev, "platform id 0x%llx is not MT8901, DVFSRC flag not written\n", + priv->plid); + /* * The sysfs nodes are debug/test hooks. Their creation must not fail * probe or disable the driver, so log a warning and continue if the From b5c4df47f2803e15b65eb2b73f4d2d0bba7ac1ff Mon Sep 17 00:00:00 2001 From: "sanket.diwate" Date: Thu, 3 Sep 2026 22:49:32 -0700 Subject: [PATCH 7/9] NVIDIA: SAUCE: soc: mediatek: power_wrap: request AC_BESTPERF mode during boot Request the AC_BESTPERF performance mode from the SSPM during boot so the platform boots in the best performance mode on AC power, matching the intended out of box behavior. Signed-off-by: sanket.diwate Signed-off-by: David Cemin --- drivers/soc/mediatek/power_wrap/power_wrap.c | 59 ++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/drivers/soc/mediatek/power_wrap/power_wrap.c b/drivers/soc/mediatek/power_wrap/power_wrap.c index 660c00992df5b..15b9fca539d6b 100644 --- a/drivers/soc/mediatek/power_wrap/power_wrap.c +++ b/drivers/soc/mediatek/power_wrap/power_wrap.c @@ -64,6 +64,21 @@ static struct pwrap_dev_ctrl dev_ctrl; #define PWRAP_DVFSRC_FLAG_ADDR 0x12FC00 #define PWRAP_DVFSRC_FLAG_LEN 0x4 #define PWRAP_DVFSRC_FLAG_VAL 0x1 +/* + * BestPerf performance setting register. + * + * A single 32-bit MMIO register that selects the SoC performance profile. + * Writing PWRAP_BEST_PERF_VAL puts the platform into its highest-performance + * operating point. The address/length/value are fixed platform constants, so + * they are hardcoded here. + * + * ioremap(ADDR, LEN) maps the register and writel(VAL) performs one 32-bit + * write; LEN is the size of the mapped window (4 bytes = one 32-bit register), + * NOT the value being written. + */ +#define PWRAP_PERF_SETTING_ADDR 0x1C871E00 +#define PWRAP_PERF_SETTING_LEN 0x4 +#define PWRAP_BEST_PERF_VAL 0x4 /* Getter for static pwrap_dev_ctrl */ struct pwrap_dev_ctrl *pwrap_get_dev_ctrl(void) @@ -514,6 +529,41 @@ static void pwrap_write_dvfsrc_flag(struct device *dev) iounmap(vaddr); } +/** + * pwrap_write_bestperf_flag() - Select the highest SoC performance profile + * @dev: device (used for logging) + * + * Writes PWRAP_BEST_PERF_VAL to the BestPerf setting register so the platform + * comes up in its highest-performance operating point. The register address, + * length, and value are fixed platform constants (see PWRAP_PERF_SETTING_* + * above). + * + * + * Failure to map the register is non-fatal: the driver continues so the SCMI + * device-control path stays functional. + */ +static void pwrap_write_bestperf_flag(struct device *dev) +{ + void __iomem *vaddr; + + vaddr = ioremap(PWRAP_PERF_SETTING_ADDR, PWRAP_PERF_SETTING_LEN); + if (!vaddr) { + dev_warn(dev, + "Perf: failed to map physical address 0x%x\n", + PWRAP_PERF_SETTING_ADDR); + return; + } + + writel(PWRAP_BEST_PERF_VAL, vaddr); + + dev_info(dev, + "BestPerf Mode enabled via address phys=0x%x len=0x%x val=0x%x\n", + PWRAP_PERF_SETTING_ADDR, PWRAP_PERF_SETTING_LEN, + PWRAP_BEST_PERF_VAL); + + iounmap(vaddr); +} + static int mtk_pwrap_probe(struct platform_device *pdev) { const struct acpi_device_id *id; @@ -563,6 +613,15 @@ static int mtk_pwrap_probe(struct platform_device *pdev) dev_info(dev, "platform id 0x%llx is not MT8901, DVFSRC flag not written\n", priv->plid); + /* + * Set performace mode to BestPerf setting by default on driver probe + */ + if (priv->plid == PWRAP_PLID_MT8901) + pwrap_write_bestperf_flag(dev); + else + dev_info(dev, "platform id 0x%llx is not MT8901, BestPerf not requested\n", + priv->plid); + /* * The sysfs nodes are debug/test hooks. Their creation must not fail * probe or disable the driver, so log a warning and continue if the From 2d858a7d31c6fdfaad7278be1b25c27327adb235 Mon Sep 17 00:00:00 2001 From: "mayuresh.wani" Date: Thu, 3 Sep 2026 22:49:32 -0700 Subject: [PATCH 8/9] NVIDIA: SAUCE: soc: mediatek: power_wrap: release usb4 resources via SSPM Release the usb4 resources through the SSPM so the platform can enter its deepest sleep states when usb4 is idle. Signed-off-by: mayuresh.wani Signed-off-by: David Cemin --- drivers/soc/mediatek/power_wrap/power_wrap.c | 78 ++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/drivers/soc/mediatek/power_wrap/power_wrap.c b/drivers/soc/mediatek/power_wrap/power_wrap.c index 15b9fca539d6b..ded37a86d3054 100644 --- a/drivers/soc/mediatek/power_wrap/power_wrap.c +++ b/drivers/soc/mediatek/power_wrap/power_wrap.c @@ -64,6 +64,33 @@ static struct pwrap_dev_ctrl dev_ctrl; #define PWRAP_DVFSRC_FLAG_ADDR 0x12FC00 #define PWRAP_DVFSRC_FLAG_LEN 0x4 #define PWRAP_DVFSRC_FLAG_VAL 0x1 + +/* + * USB4 resource-release hook. + * + * The bootloader leaves the USB4 hardware resources powered on when USB4 is + * not used. A single SCMI trigger to the SSPM (feature id 15, no params) tells + * the firmware to release them. This was previously a standalone one-call + * driver (usb4_hook.c / CONFIG_MTK_USB4_HOOK); it is folded into power_wrap + * here to avoid the overhead of a separate driver for a single API call. + * + * The release is issued from a late_initcall so it runs after the xHCI driver + * (device_initcall) that owns the USB4 domain; issuing it earlier lets xHCI + * re-power the domain and blocks the deepest suspend state. probe() stashes + * the device; the late_initcall consumes it. + * + * The release is restricted to MT8901. The feature-id 15 trigger encodes + * an MT8901 USB4 resource-layout assumption, so gating on the platform id lets + * other platforms (e.g. MT8992) boot and be tested without reverting this + * change. The platform id is read from the PLID method on the power_wrap ACPI + * device (NVDA6210), mirroring the Windows PEP driver's AcpiGetPlatformId(); + * the value matches the PEP PLAT_IDENTIFIER enum (SOC_MT8901 == 0x2). + */ +#define PWRAP_USB4_HOOK_FEATURE_ID 15 /* SSPM SCMI_USB4_HOOK */ + +/* Device stashed by probe for the USB4-release late_initcall. */ +static struct device *pwrap_usb4_dev; + /* * BestPerf performance setting register. * @@ -564,6 +591,51 @@ static void pwrap_write_bestperf_flag(struct device *dev) iounmap(vaddr); } +/* + * pwrap_release_usb4_resources() - release USB4 hardware resources via SSPM + * + * Sends a single SCMI trigger (feature id 15, no params) telling the firmware + * to release the USB4 resources the bootloader leaves powered on. Restricted + * to MT8901 via the PLID platform id: on any other platform, or if PLID is not + * available, the trigger is skipped so the platform is unaffected. Runs as a + * late_initcall (see the USB4 hook note above); does nothing if power_wrap did + * not probe. Best-effort: a release failure is logged but not escalated. + */ +static int __init pwrap_release_usb4_resources(void) +{ + struct device *dev = pwrap_usb4_dev; + unsigned long long plid; + acpi_status status; + int ret; + + if (!dev) + return 0; + + status = acpi_evaluate_integer(ACPI_HANDLE(dev), "PLID", NULL, &plid); + if (ACPI_FAILURE(status)) { + dev_info(dev, "USB4: PLID unavailable (%s), skipping release\n", + acpi_format_exception(status)); + return 0; + } + + if (plid != PWRAP_PLID_MT8901) { + dev_info(dev, "USB4: platform id 0x%llx is not MT8901, skipping release\n", + plid); + return 0; + } + + ret = sspm_ci_set(PWRAP_USB4_HOOK_FEATURE_ID, 0, 0, 0, 0, 0); + if (ret) + dev_err(dev, "USB4: sspm_ci_set failed to release resources (%d)\n", + ret); + else + dev_info(dev, "USB4: SSPM released usb4 resources\n"); + + /* Best-effort: a release failure is logged but not escalated. */ + return 0; +} +late_initcall(pwrap_release_usb4_resources); + static int mtk_pwrap_probe(struct platform_device *pdev) { const struct acpi_device_id *id; @@ -632,6 +704,12 @@ static int mtk_pwrap_probe(struct platform_device *pdev) dev_warn(&pdev->dev, "failed to create sysfs nodes: %d (continuing)\n", ret); + /* + * Stash the device for the USB4-release late_initcall, which runs after + * the xHCI driver has probed (see the USB4 hook note above). + */ + pwrap_usb4_dev = dev; + return 0; } From fa1849cd8855e01851a141799c19a7a1cb062168 Mon Sep 17 00:00:00 2001 From: David Cemin Date: Thu, 3 Sep 2026 22:55:46 -0700 Subject: [PATCH 9/9] UBUNTU: [Config] annotations for the MediaTek SSPM and power_wrap drivers Add the config annotations for MTK_SSPM_CI and MTK_POWER_WRAP. Signed-off-by: David Cemin --- debian.nvidia-bos/config/annotations | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian.nvidia-bos/config/annotations b/debian.nvidia-bos/config/annotations index cf6d9d8c55694..a7857b7b8c557 100644 --- a/debian.nvidia-bos/config/annotations +++ b/debian.nvidia-bos/config/annotations @@ -219,6 +219,12 @@ CONFIG_MTD note<'Essential for boot on ARM6 CONFIG_MTK_PCIE_HOTPLUG policy<{'arm64': 'm'}> CONFIG_MTK_PCIE_HOTPLUG note<'CX7 PCIe hotplug driver for NVIDIA DGX Spark systems with GB10 SoC.'> +CONFIG_MTK_POWER_WRAP policy<{'arm64': 'y'}> +CONFIG_MTK_POWER_WRAP note<'MediaTek Power Wrap device-control (SCMI) driver.'> + +CONFIG_MTK_SSPM_CI policy<{'arm64': 'y'}> +CONFIG_MTK_SSPM_CI note<'MediaTek SSPM control interface used by the Power Wrap SCMI device-control path.'> + CONFIG_NOUVEAU_DEBUG policy<{'amd64': '-', 'arm64': '-'}> CONFIG_NOUVEAU_DEBUG note<'Disable nouveau for NVIDIA kernels'>