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'> diff --git a/drivers/soc/mediatek/Kconfig b/drivers/soc/mediatek/Kconfig index d7293977f06e8..b20141abb96ee 100644 --- a/drivers/soc/mediatek/Kconfig +++ b/drivers/soc/mediatek/Kconfig @@ -89,4 +89,22 @@ 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. +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 0665573e3c4b3..91d8b58d9faff 100644 --- a/drivers/soc/mediatek/Makefile +++ b/drivers/soc/mediatek/Makefile @@ -9,3 +9,5 @@ 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/ +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..0765daf225261 --- /dev/null +++ b/drivers/soc/mediatek/power_wrap/Makefile @@ -0,0 +1,10 @@ +# 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 += power_wrap_sysfs.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..a2b64a4c7926f --- /dev/null +++ b/drivers/soc/mediatek/power_wrap/inc/power_wrap.h @@ -0,0 +1,156 @@ +/* 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; + /* 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) */ +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..ded37a86d3054 --- /dev/null +++ b/drivers/soc/mediatek/power_wrap/power_wrap.c @@ -0,0 +1,759 @@ +// 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; + +/* + * 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 + +/* + * 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. + * + * 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) +{ + 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); + +/** + * 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; + + 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; +} + +/** + * 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); +} + +/** + * 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); +} + +/* + * 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; + 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 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); + + /* + * 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 + * 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); + + /* + * 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; +} + +#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/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..fd7f67ac1a862 --- /dev/null +++ b/drivers/soc/mediatek/power_wrap/power_wrap_sysfs.c @@ -0,0 +1,568 @@ +// 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); + +/* + * 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, + &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, + &dev_attr_pwrap_power_ctrl_req.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); +} 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_ */ diff --git a/include/linux/soc/mediatek/mtk-pwrap.h b/include/linux/soc/mediatek/mtk-pwrap.h new file mode 100644 index 0000000000000..16d98e387c6f2 --- /dev/null +++ b/include/linux/soc/mediatek/mtk-pwrap.h @@ -0,0 +1,121 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2026 MediaTek Inc. + */ +#ifndef __MTK_PWRAP_PUBLIC_H__ +#define __MTK_PWRAP_PUBLIC_H__ + +#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, + 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_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); +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_send_power_control_req(struct _power_cntrl_request *pwrctlreq) +{ + return -ENODEV; +} + +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