* [PATCH v2 0/3] Add Qualcomm SPEL powercap driver
@ 2026-06-19 20:39 Manaf Meethalavalappu Pallikunhi
2026-06-19 20:39 ` [PATCH v2 1/3] dt-bindings: power: limits: Describe Qualcomm SPEL hardware Manaf Meethalavalappu Pallikunhi
` (2 more replies)
0 siblings, 3 replies; 5+ messages in thread
From: Manaf Meethalavalappu Pallikunhi @ 2026-06-19 20:39 UTC (permalink / raw)
To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Rafael J. Wysocki,
Bjorn Andersson, Konrad Dybcio, Daniel Lezcano
Cc: Gaurav Kohli, linux-arm-msm, devicetree, linux-kernel, linux-pm,
Manaf Meethalavalappu Pallikunhi
This patch series introduces support for Qualcomm's SPEL (SoC Power
and Electrical Limits) hardware, which provides power monitoring and
limiting capabilities for various power domains in Qualcomm SoCs.
The SPEL hardware enables:
- Real-time energy consumption monitoring across multiple power domains
- Hardware-enforced power limits with configurable time windows
- Support for up to 4 power limits (PL1-PL4) per domain
- Hierarchical power domain organization (SYS -> SOC -> subsystems)
The driver integrates with the Linux powercap framework, exposing SPEL
capabilities through standard sysfs interfaces. This allows userspace
applications and thermal management daemons to monitor energy consumption
and configure power limits for optimal power/performance balance.
Signed-off-by: Manaf Meethalavalappu Pallikunhi <manaf.pallikunhi@oss.qualcomm.com>
---
Changes in v2:
- Fixed DT issues: corrected unit-address mismatch and made compatible
platform-specific.(Konrad/Krzysztof)
- Cleaned up bitfield handling: switched to GENMASK, FIELD_* macros.(Konrad)
- Converted hex values to lowercase and simplified array definitions.(Konrad)
- used devm_platform_ioremap_resource_byname().(Konrad)
- Refactored data structures: merged domain data into spel_domain_info
and removed hardcoded limits using ARRAY_SIZE().(Daniel)
- Added bounds checks (TIME_WINDOW_MAX) and fixed time window RMW
handling (preserving reserved bits + firmware notify).(Sashiko)
- Resolved use-after-free, replaced allocation strategy, and
added proper cleanup in .release().(Sashiko)
- Updated time window calculation logic.
- Removed cyclic dependency, improved validation logic, and aligned
naming consistency.(Daniel)
- Link to v1: https://lore.kernel.org/r/20260519-qcom_spel_driver_upstream-v1-0-75356d1b7f94@oss.qualcomm.com
---
Manaf Meethalavalappu Pallikunhi (3):
dt-bindings: power: limits: Describe Qualcomm SPEL hardware
powercap: qcom: Add SPEL powercap driver
arm64: dts: qcom: glymur: Enable SPEL powercap driver
.../bindings/power/limits/qcom,spel.yaml | 47 ++
MAINTAINERS | 7 +
arch/arm64/boot/dts/qcom/glymur.dtsi | 10 +
drivers/powercap/Kconfig | 13 +
drivers/powercap/Makefile | 1 +
drivers/powercap/qcom_spel.c | 776 +++++++++++++++++++++
6 files changed, 854 insertions(+)
---
base-commit: 80dd246accce631c328ea43294e53b2b2dd2aa32
change-id: 20260519-qcom_spel_driver_upstream-11efd8c85e6e
Best regards,
--
Manaf Meethalavalappu Pallikunhi <manaf.pallikunhi@oss.qualcomm.com>
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 1/3] dt-bindings: power: limits: Describe Qualcomm SPEL hardware
2026-06-19 20:39 [PATCH v2 0/3] Add Qualcomm SPEL powercap driver Manaf Meethalavalappu Pallikunhi
@ 2026-06-19 20:39 ` Manaf Meethalavalappu Pallikunhi
2026-06-19 20:39 ` [PATCH v2 2/3] powercap: qcom: Add SPEL powercap driver Manaf Meethalavalappu Pallikunhi
2026-06-19 20:39 ` [PATCH v2 3/3] arm64: dts: qcom: glymur: Enable " Manaf Meethalavalappu Pallikunhi
2 siblings, 0 replies; 5+ messages in thread
From: Manaf Meethalavalappu Pallikunhi @ 2026-06-19 20:39 UTC (permalink / raw)
To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Rafael J. Wysocki,
Bjorn Andersson, Konrad Dybcio, Daniel Lezcano
Cc: Gaurav Kohli, linux-arm-msm, devicetree, linux-kernel, linux-pm,
Manaf Meethalavalappu Pallikunhi
The Qualcomm SoC Power and Electrical Limits (SPEL) provides hardware
based power monitoring and limiting capabilities for various domains.
Add a DeviceTree binding to describe the SPEL block on Qualcomm's SoC.
Signed-off-by: Manaf Meethalavalappu Pallikunhi <manaf.pallikunhi@oss.qualcomm.com>
---
.../bindings/power/limits/qcom,spel.yaml | 47 ++++++++++++++++++++++
MAINTAINERS | 6 +++
2 files changed, 53 insertions(+)
diff --git a/Documentation/devicetree/bindings/power/limits/qcom,spel.yaml b/Documentation/devicetree/bindings/power/limits/qcom,spel.yaml
new file mode 100644
index 000000000000..4c6e6cbfbfe4
--- /dev/null
+++ b/Documentation/devicetree/bindings/power/limits/qcom,spel.yaml
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/power/limits/qcom,spel.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Qualcomm SoC Power and Electrical Limits (SPEL)
+
+maintainers:
+ - Manaf Meethalavalappu Pallikunhi <manaf.pallikunhi@oss.qualcomm.com>
+
+description:
+ The Qualcomm SPEL (SoC Power and Electrical Limits) provides hardware-based
+ power monitoring and limiting capabilities for various power domains in
+ Qualcomm SoCs.
+
+properties:
+ compatible:
+ const: qcom,glymur-spel
+
+ reg:
+ maxItems: 3
+
+ reg-names:
+ items:
+ - const: config
+ - const: constraints
+ - const: nodes
+
+required:
+ - compatible
+ - reg
+ - reg-names
+
+additionalProperties: false
+
+examples:
+ - |
+ power-limits@ef3b000 {
+ compatible = "qcom,glymur-spel";
+ reg = <0x0ef3b000 0x1000>,
+ <0x0ef3d000 0x1000>,
+ <0x0ef3e000 0x1000>;
+ reg-names = "config",
+ "constraints",
+ "nodes";
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 3dd58a16f06a..c63f147e8c54 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22233,6 +22233,12 @@ S: Maintained
F: Documentation/devicetree/bindings/power/supply/qcom,pmi8998-charger.yaml
F: drivers/power/supply/qcom_smbx.c
+QUALCOMM SPEL POWERCAP DRIVER
+M: Manaf Meethalavalappu Pallikunhi <manaf.pallikunhi@oss.qualcomm.com>
+L: linux-arm-msm@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/power/limits/qcom,spel.yaml
+
QUALCOMM PPE DRIVER
M: Luo Jie <quic_luoj@quicinc.com>
L: netdev@vger.kernel.org
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v2 2/3] powercap: qcom: Add SPEL powercap driver
2026-06-19 20:39 [PATCH v2 0/3] Add Qualcomm SPEL powercap driver Manaf Meethalavalappu Pallikunhi
2026-06-19 20:39 ` [PATCH v2 1/3] dt-bindings: power: limits: Describe Qualcomm SPEL hardware Manaf Meethalavalappu Pallikunhi
@ 2026-06-19 20:39 ` Manaf Meethalavalappu Pallikunhi
2026-06-19 20:50 ` sashiko-bot
2026-06-19 20:39 ` [PATCH v2 3/3] arm64: dts: qcom: glymur: Enable " Manaf Meethalavalappu Pallikunhi
2 siblings, 1 reply; 5+ messages in thread
From: Manaf Meethalavalappu Pallikunhi @ 2026-06-19 20:39 UTC (permalink / raw)
To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Rafael J. Wysocki,
Bjorn Andersson, Konrad Dybcio, Daniel Lezcano
Cc: Gaurav Kohli, linux-arm-msm, devicetree, linux-kernel, linux-pm,
Manaf Meethalavalappu Pallikunhi
The Qualcomm SoC Power and Electrical Limits (SPEL) provides hardware
based power monitoring and limiting capabilities for various power
domains including System, SoC, CPU clusters, GPU, and various other
subsystems.
The driver integrates with the Linux powercap framework, exposing SPEL
capabilities through powercap sysfs interfaces.
Signed-off-by: Manaf Meethalavalappu Pallikunhi <manaf.pallikunhi@oss.qualcomm.com>
---
MAINTAINERS | 1 +
drivers/powercap/Kconfig | 13 +
drivers/powercap/Makefile | 1 +
drivers/powercap/qcom_spel.c | 776 +++++++++++++++++++++++++++++++++++++++++++
4 files changed, 791 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index c63f147e8c54..5c7542754ab6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22238,6 +22238,7 @@ M: Manaf Meethalavalappu Pallikunhi <manaf.pallikunhi@oss.qualcomm.com>
L: linux-arm-msm@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/power/limits/qcom,spel.yaml
+F: drivers/powercap/qcom_spel.c
QUALCOMM PPE DRIVER
M: Luo Jie <quic_luoj@quicinc.com>
diff --git a/drivers/powercap/Kconfig b/drivers/powercap/Kconfig
index 03c4c796d993..e3a47c653499 100644
--- a/drivers/powercap/Kconfig
+++ b/drivers/powercap/Kconfig
@@ -93,4 +93,17 @@ config DTPM_DEVFREQ
help
This enables support for device power limitation based on
energy model.
+
+config QCOM_SPEL
+ tristate "Qualcomm SPEL Powercap driver"
+ depends on ARM64 || COMPILE_TEST
+ help
+ This enables support for the Qualcomm SoC Power and Electrical
+ Limits (SPEL) hardware, which allows power limits to be
+ enforced and monitored on Qualcomm SoCs.
+
+ SPEL provides energy monitoring and power capping for multiple
+ domains including system, SoC, CPU clusters, GPU, and various
+ other subsystems.
+
endif
diff --git a/drivers/powercap/Makefile b/drivers/powercap/Makefile
index 5ab0dce565b9..8235fb9d3df6 100644
--- a/drivers/powercap/Makefile
+++ b/drivers/powercap/Makefile
@@ -8,3 +8,4 @@ obj-$(CONFIG_INTEL_RAPL) += intel_rapl_msr.o
obj-$(CONFIG_INTEL_RAPL_TPMI) += intel_rapl_tpmi.o
obj-$(CONFIG_IDLE_INJECT) += idle_inject.o
obj-$(CONFIG_ARM_SCMI_POWERCAP) += arm_scmi_powercap.o
+obj-$(CONFIG_QCOM_SPEL) += qcom_spel.o
diff --git a/drivers/powercap/qcom_spel.c b/drivers/powercap/qcom_spel.c
new file mode 100644
index 000000000000..4dd91cf36ccc
--- /dev/null
+++ b/drivers/powercap/qcom_spel.c
@@ -0,0 +1,776 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Qualcomm SPEL (SoC Power and Electrical Limits) Driver
+ *
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/device.h>
+#include <linux/io.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mod_devicetable.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/powercap.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+
+/* SPEL register bitmasks */
+#define ENERGY_STATUS_MASK GENMASK(31, 0)
+
+#define POWER_LIMIT_MASK GENMASK(14, 0)
+#define POWER_LIMIT_ENABLE BIT(31)
+
+#define TIME_WINDOW_MASK_L GENMASK(14, 0)
+#define TIME_WINDOW_MASK_H GENMASK(22, 16)
+#define TIME_WINDOW_MAX ((FIELD_MAX(TIME_WINDOW_MASK_H) << 15) | \
+ FIELD_MAX(TIME_WINDOW_MASK_L))
+
+#define ENERGY_UNIT_MASK GENMASK(19, 16)
+#define TIME_UNIT_MASK GENMASK(11, 8)
+#define POWER_UNIT_MASK GENMASK(2, 0)
+
+#define LIMITS_CAPABILITY_OFFSET 0x20
+#define ENERGY_RPT_UNIT_OFFSET 0x04
+
+#define ENERGY_UNIT_SCALE 1000
+
+#define SPEL_DOMAIN_NAME_LENGTH 16
+
+/* Domain types */
+enum spel_domain_type {
+ SPEL_DOMAIN_SYS,
+ SPEL_DOMAIN_SOC,
+ SPEL_DOMAIN_CL0,
+ SPEL_DOMAIN_CL1,
+ SPEL_DOMAIN_CL2,
+ SPEL_DOMAIN_IGPU,
+ SPEL_DOMAIN_DGPU,
+ SPEL_DOMAIN_NSP,
+ SPEL_DOMAIN_MMCX,
+ SPEL_DOMAIN_INFRA,
+ SPEL_DOMAIN_DRAM,
+ SPEL_DOMAIN_MDM,
+ SPEL_DOMAIN_WLAN,
+ SPEL_DOMAIN_USB1,
+ SPEL_DOMAIN_USB2,
+ SPEL_DOMAIN_USB3,
+ SPEL_DOMAIN_MAX,
+};
+
+/* Power limit IDs */
+enum spel_power_limit_id {
+ POWER_LIMIT1,
+ POWER_LIMIT2,
+ POWER_LIMIT3,
+ POWER_LIMIT4,
+ POWER_LIMITS_MAX,
+};
+
+/* Unit types for conversion */
+enum unit_type {
+ POWER_UNIT,
+ ENERGY_UNIT,
+ TIME_UNIT,
+};
+
+/* Power limit operation types */
+enum pl_ops_type {
+ PL_LIMIT,
+ PL_TIME_WINDOW,
+};
+
+static const char * const pl_names[] = {
+ [POWER_LIMIT1] = "pl1",
+ [POWER_LIMIT2] = "pl2",
+ [POWER_LIMIT3] = "pl3",
+ [POWER_LIMIT4] = "pl4",
+};
+
+/**
+ * struct spel_domain_info - Domain configuration
+ * @name: Domain name
+ * @offset: Register offset in node base
+ */
+struct spel_domain_info {
+ const char *name;
+ u32 offset;
+};
+
+/* Domain configuration */
+static const struct spel_domain_info domain_info[SPEL_DOMAIN_MAX] = {
+ [SPEL_DOMAIN_SYS] = { "sys", 0x40 },
+ [SPEL_DOMAIN_SOC] = { "soc", 0x00 },
+ [SPEL_DOMAIN_CL0] = { "cl0", 0x5c },
+ [SPEL_DOMAIN_CL1] = { "cl1", 0x60 },
+ [SPEL_DOMAIN_CL2] = { "cl2", 0x64 },
+ [SPEL_DOMAIN_IGPU] = { "igpu", 0x08 },
+ [SPEL_DOMAIN_DGPU] = { "dgpu", 0x44 },
+ [SPEL_DOMAIN_NSP] = { "nsp", 0x0c },
+ [SPEL_DOMAIN_MMCX] = { "mmcx", 0x10 },
+ [SPEL_DOMAIN_INFRA] = { "infra", 0x18 },
+ [SPEL_DOMAIN_DRAM] = { "dram", 0x1c },
+ [SPEL_DOMAIN_MDM] = { "mdm", 0x48 },
+ [SPEL_DOMAIN_WLAN] = { "wlan", 0x4c },
+ [SPEL_DOMAIN_USB1] = { "usb1", 0x50 },
+ [SPEL_DOMAIN_USB2] = { "usb2", 0x54 },
+ [SPEL_DOMAIN_USB3] = { "usb3", 0x58 },
+};
+
+/**
+ * struct spel_constraint_info - Power limit constraint information
+ * @limit_offset: Register offset for power limit value
+ * @time_window_offset: Register offset for time window
+ * @supported_mask: Bit mask in capability register
+ * @domain_id: Domain this constraint applies to
+ * @pl_id: Power limit ID (PL1, PL2, etc.)
+ */
+struct spel_constraint_info {
+ u32 limit_offset;
+ u32 time_window_offset;
+ u32 supported_mask;
+ enum spel_domain_type domain_id;
+ int pl_id;
+};
+
+/* Constraint configuration */
+static const struct spel_constraint_info constraints[] = {
+ /* SYS domain constraints */
+ { 0x10, 0x70, BIT(0), SPEL_DOMAIN_SYS, POWER_LIMIT1 },
+ { 0x14, 0x74, BIT(1), SPEL_DOMAIN_SYS, POWER_LIMIT2 },
+ { 0x18, 0x78, BIT(2), SPEL_DOMAIN_SYS, POWER_LIMIT3 },
+ { 0x1c, 0x7c, BIT(3), SPEL_DOMAIN_SYS, POWER_LIMIT4 },
+ /* SoC domain constraints */
+ { 0x00, 0x60, BIT(4), SPEL_DOMAIN_SOC, POWER_LIMIT1 },
+ { 0x04, 0x64, BIT(5), SPEL_DOMAIN_SOC, POWER_LIMIT2 },
+ { 0x08, 0x68, BIT(6), SPEL_DOMAIN_SOC, POWER_LIMIT3 },
+ { 0x0c, 0x6c, BIT(7), SPEL_DOMAIN_SOC, POWER_LIMIT4 },
+};
+
+/**
+ * struct spel_domain - SPEL power domain
+ * @power_zone: Powercap zone
+ * @lock: Mutex protecting register access
+ * @sp: Parent sys domain
+ * @status_reg: Energy counter register
+ * @name: Domain name
+ * @id: Domain type ID
+ */
+struct spel_domain {
+ struct powercap_zone power_zone;
+ struct mutex lock; /* Protects register read/write operations */
+ void *sp;
+ void __iomem *status_reg;
+ char name[SPEL_DOMAIN_NAME_LENGTH];
+ enum spel_domain_type id;
+};
+
+/**
+ * struct spel_system - SPEL system
+ * @domains: Array of domains
+ * @power_zone: Parent powercap zone
+ * @node_base: Base address for node registers
+ * @constraint_base: Base address for constraint registers
+ * @config_base: Base address for config registers
+ * @control_type: Powercap control type
+ * @dev: Device pointer for logging
+ * @limits: Supported power limits per domain
+ * @power_unit: Power unit in microWatts (common for all domains)
+ * @energy_unit: Energy unit in nanoJoules (common for all domains)
+ * @time_unit: Time unit in microseconds (common for all domains)
+ */
+struct spel_system {
+ struct spel_domain *domains;
+ struct powercap_zone *power_zone;
+ void __iomem *node_base;
+ void __iomem *constraint_base;
+ void __iomem *config_base;
+ struct powercap_control_type *control_type;
+ struct device *dev;
+ int limits[SPEL_DOMAIN_MAX];
+ unsigned int power_unit;
+ unsigned int energy_unit;
+ unsigned int time_unit;
+};
+
+#define power_zone_to_spel_domain(_zone) \
+ container_of(_zone, struct spel_domain, power_zone)
+
+static bool is_pl_valid(struct spel_domain *sd, int pl)
+{
+ struct spel_system *sp = sd->sp;
+
+ return !!(sp->limits[sd->id] & BIT(pl));
+}
+
+static int get_pl_ops_offset(struct spel_domain *sd, int pl, enum pl_ops_type pl_op)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(constraints); i++) {
+ const struct spel_constraint_info *ci = &constraints[i];
+
+ if (ci->domain_id == sd->id && ci->pl_id == pl) {
+ switch (pl_op) {
+ case PL_LIMIT:
+ return ci->limit_offset;
+ case PL_TIME_WINDOW:
+ return ci->time_window_offset;
+ default:
+ return -EOPNOTSUPP;
+ }
+ }
+ }
+
+ return -EOPNOTSUPP;
+}
+
+static u64 spel_unit_xlate(struct spel_domain *sd, enum unit_type type,
+ u64 value, int to_raw)
+{
+ struct spel_system *sp = sd->sp;
+ u64 units, scale;
+
+ switch (type) {
+ case POWER_UNIT:
+ units = sp->power_unit;
+ scale = 1;
+ break;
+ case ENERGY_UNIT:
+ units = sp->energy_unit;
+ scale = ENERGY_UNIT_SCALE;
+ break;
+ case TIME_UNIT:
+ units = sp->time_unit;
+ scale = 1;
+ break;
+ default:
+ return value;
+ }
+
+ if (to_raw)
+ return DIV_ROUND_CLOSEST_ULL(value * scale, units);
+
+ value *= units;
+ return div64_u64(value, scale);
+}
+
+static int spel_read_pl_data(struct spel_domain *sd, int pl,
+ enum pl_ops_type pl_op, bool xlate, u64 *data)
+{
+ struct spel_system *sp = sd->sp;
+ void __iomem *reg_addr;
+ u64 value;
+ int offset;
+
+ if (!is_pl_valid(sd, pl))
+ return -EINVAL;
+
+ offset = get_pl_ops_offset(sd, pl, pl_op);
+ if (offset < 0)
+ return offset;
+
+ guard(mutex)(&sd->lock);
+
+ reg_addr = sp->constraint_base + offset;
+ value = readl(reg_addr);
+
+ switch (pl_op) {
+ case PL_LIMIT:
+ value = FIELD_GET(POWER_LIMIT_MASK, value);
+ if (xlate)
+ *data = spel_unit_xlate(sd, POWER_UNIT, value, 0);
+ else
+ *data = value;
+ break;
+ case PL_TIME_WINDOW:
+ /* Decode time window: bits [22:16] are upper 7 bits, [14:0] are lower 15 bits */
+ value = (FIELD_GET(TIME_WINDOW_MASK_H, value) << 15) |
+ FIELD_GET(TIME_WINDOW_MASK_L, value);
+ if (xlate)
+ *data = spel_unit_xlate(sd, TIME_UNIT, value, 0);
+ else
+ *data = value;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int spel_write_pl_data(struct spel_domain *sd, int pl,
+ enum pl_ops_type pl_op, unsigned long long value)
+{
+ struct spel_system *sp = sd->sp;
+ void __iomem *reg_addr;
+ u64 reg_val, new_val;
+ int offset;
+
+ if (!is_pl_valid(sd, pl))
+ return -EINVAL;
+
+ offset = get_pl_ops_offset(sd, pl, pl_op);
+ if (offset < 0)
+ return offset;
+
+ guard(mutex)(&sd->lock);
+
+ reg_addr = sp->constraint_base + offset;
+ reg_val = readl(reg_addr);
+
+ switch (pl_op) {
+ case PL_LIMIT:
+ new_val = spel_unit_xlate(sd, POWER_UNIT, value, 1);
+ if (new_val > FIELD_MAX(POWER_LIMIT_MASK))
+ return -EINVAL;
+ reg_val = (reg_val & ~POWER_LIMIT_MASK) | FIELD_PREP(POWER_LIMIT_MASK, new_val);
+
+ /*
+ * Enable/Disable PL based on the value:
+ * - If value is 0, disable the PL (clear enable bit)
+ * - If value is non-zero, enable the PL (set enable bit)
+ */
+ if (new_val == 0)
+ reg_val &= ~POWER_LIMIT_ENABLE;
+ else
+ reg_val |= POWER_LIMIT_ENABLE;
+
+ writel(reg_val, reg_addr);
+ return 0;
+
+ case PL_TIME_WINDOW:
+ /*
+ * Encode time window: upper 7 bits to [22:16], lower 15 bits to [14:0]
+ */
+ new_val = spel_unit_xlate(sd, TIME_UNIT, value, 1);
+ if (new_val > TIME_WINDOW_MAX)
+ return -EINVAL;
+ /* Read-modify-write to preserve other bits */
+ reg_val = (reg_val & ~(TIME_WINDOW_MASK_H | TIME_WINDOW_MASK_L)) |
+ FIELD_PREP(TIME_WINDOW_MASK_H, new_val >> 15) |
+ FIELD_PREP(TIME_WINDOW_MASK_L, new_val);
+ writel(reg_val, reg_addr);
+
+ /*
+ * Time window register update doesn't trigger firmware interrupt.
+ * Write to the PL register with current value to trigger the interrupt.
+ */
+ offset = get_pl_ops_offset(sd, pl, PL_LIMIT);
+ if (offset >= 0) {
+ reg_addr = sp->constraint_base + offset;
+ reg_val = readl(reg_addr);
+ writel(reg_val, reg_addr);
+ }
+ return 0;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static int spel_get_energy_counter(struct powercap_zone *power_zone, u64 *energy_raw)
+{
+ struct spel_domain *sd = power_zone_to_spel_domain(power_zone);
+ u64 value;
+
+ value = readl(sd->status_reg);
+
+ *energy_raw = spel_unit_xlate(sd, ENERGY_UNIT, value, 0);
+
+ return 0;
+}
+
+static int spel_get_max_energy_counter(struct powercap_zone *pcd_dev, u64 *energy)
+{
+ struct spel_domain *sd = power_zone_to_spel_domain(pcd_dev);
+
+ *energy = spel_unit_xlate(sd, ENERGY_UNIT, ENERGY_STATUS_MASK, 0);
+
+ return 0;
+}
+
+static int spel_release_zone(struct powercap_zone *power_zone)
+{
+ struct spel_domain *sd = power_zone_to_spel_domain(power_zone);
+ struct spel_system *sp = sd->sp;
+
+ /* Free the domains array when the last zone (SYS domain) is released */
+ if (sd->id == SPEL_DOMAIN_SYS)
+ kfree(sp->domains);
+
+ return 0;
+}
+
+static int spel_find_nr_power_limit(struct spel_domain *sd)
+{
+ int i, nr_pl = 0;
+
+ for (i = 0; i < ARRAY_SIZE(pl_names); i++) {
+ if (is_pl_valid(sd, i))
+ nr_pl++;
+ }
+
+ return nr_pl;
+}
+
+static const struct powercap_zone_ops zone_ops = {
+ .get_energy_uj = spel_get_energy_counter,
+ .get_max_energy_range_uj = spel_get_max_energy_counter,
+ .release = spel_release_zone,
+};
+
+static int spel_constraint_to_pl(struct spel_domain *sd, int cid)
+{
+ int i, id;
+
+ for (i = 0, id = 0; i < ARRAY_SIZE(pl_names); i++) {
+ if (is_pl_valid(sd, i) && id++ == cid)
+ return i;
+ }
+
+ return -EINVAL;
+}
+
+static int spel_set_power_limit(struct powercap_zone *power_zone, int cid,
+ u64 power_limit)
+{
+ struct spel_domain *sd = power_zone_to_spel_domain(power_zone);
+ int id;
+
+ id = spel_constraint_to_pl(sd, cid);
+ if (id < 0)
+ return id;
+
+ return spel_write_pl_data(sd, id, PL_LIMIT, power_limit);
+}
+
+static int spel_get_power_limit(struct powercap_zone *power_zone, int cid,
+ u64 *data)
+{
+ struct spel_domain *sd = power_zone_to_spel_domain(power_zone);
+ u64 val;
+ int ret, id;
+
+ id = spel_constraint_to_pl(sd, cid);
+ if (id < 0)
+ return id;
+
+ ret = spel_read_pl_data(sd, id, PL_LIMIT, true, &val);
+ if (!ret)
+ *data = val;
+
+ return ret;
+}
+
+static int spel_set_time_window(struct powercap_zone *power_zone, int cid,
+ u64 window)
+{
+ struct spel_domain *sd = power_zone_to_spel_domain(power_zone);
+ int id;
+
+ id = spel_constraint_to_pl(sd, cid);
+ if (id < 0)
+ return id;
+
+ return spel_write_pl_data(sd, id, PL_TIME_WINDOW, window);
+}
+
+static int spel_get_time_window(struct powercap_zone *power_zone, int cid,
+ u64 *data)
+{
+ struct spel_domain *sd = power_zone_to_spel_domain(power_zone);
+ u64 val;
+ int ret, id;
+
+ id = spel_constraint_to_pl(sd, cid);
+ if (id < 0)
+ return id;
+
+ ret = spel_read_pl_data(sd, id, PL_TIME_WINDOW, true, &val);
+ if (!ret)
+ *data = val;
+
+ return ret;
+}
+
+static const char *spel_get_constraint_name(struct powercap_zone *power_zone,
+ int cid)
+{
+ struct spel_domain *sd = power_zone_to_spel_domain(power_zone);
+ int id;
+
+ id = spel_constraint_to_pl(sd, cid);
+ if (id >= 0 && id < ARRAY_SIZE(pl_names))
+ return pl_names[id];
+
+ return NULL;
+}
+
+static const struct powercap_zone_constraint_ops constraint_ops = {
+ .set_power_limit_uw = spel_set_power_limit,
+ .get_power_limit_uw = spel_get_power_limit,
+ .set_time_window_us = spel_set_time_window,
+ .get_time_window_us = spel_get_time_window,
+ .get_name = spel_get_constraint_name,
+};
+
+static void spel_init_domains(struct spel_system *sp)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(domain_info); i++) {
+ struct spel_domain *sd = &sp->domains[i];
+
+ sd->sp = sp;
+ snprintf(sd->name, SPEL_DOMAIN_NAME_LENGTH, "%s",
+ domain_info[i].name);
+ sd->id = i;
+ sd->status_reg = sp->node_base + domain_info[i].offset;
+
+ /* PL1 is always supported (required for powercap registration) */
+ sp->limits[i] = BIT(POWER_LIMIT1);
+ }
+}
+
+static void spel_update_unit(struct spel_system *sp)
+{
+ u32 value, shift;
+
+ /* Read power_unit and time_unit from offset 0x0 */
+ value = readl(sp->config_base);
+
+ /*
+ * Unit calculation: 1 / (2^shift)
+ * Masks limit: TIME_UNIT (4 bits, max 15), POWER_UNIT (3 bits, max 7).
+ */
+ shift = FIELD_GET(POWER_UNIT_MASK, value);
+ sp->power_unit = 1000000 / (1 << shift);
+
+ shift = FIELD_GET(TIME_UNIT_MASK, value);
+ /*
+ * Time window in register is in milliseconds.
+ */
+ sp->time_unit = 1000 * (1 << shift);
+
+ /* Read energy_unit from ENERGY_RPT_UNIT_OFFSET */
+ value = readl(sp->config_base + ENERGY_RPT_UNIT_OFFSET);
+
+ /*
+ * Unit calculation: 1 / (2^shift)
+ * Masks limit: ENERGY_UNIT (4 bits, max 15).
+ */
+ shift = FIELD_GET(ENERGY_UNIT_MASK, value);
+ sp->energy_unit = ENERGY_UNIT_SCALE * 1000000 / (1 << shift);
+
+ dev_dbg(sp->dev, "Units: energy=%dnJ, time=%dus, power=%duW\n",
+ sp->energy_unit, sp->time_unit, sp->power_unit);
+}
+
+static void spel_detect_powerlimit(struct spel_domain *sd)
+{
+ struct spel_system *sp = sd->sp;
+ u32 capabilities;
+ int i, j;
+
+ capabilities = readl(sp->config_base + LIMITS_CAPABILITY_OFFSET);
+
+ /*
+ * Detect power limits from hardware capabilities.
+ * Start from index 1 (POWER_LIMIT2) since PL1 is always enabled in spel_init_domains().
+ */
+ for (i = 1; i < ARRAY_SIZE(pl_names); i++) {
+ for (j = 0; j < ARRAY_SIZE(constraints); j++) {
+ const struct spel_constraint_info *ci = &constraints[j];
+
+ if (ci->domain_id == sd->id && ci->pl_id == i) {
+ if (capabilities & ci->supported_mask)
+ sp->limits[sd->id] |= BIT(i);
+ break;
+ }
+ }
+ }
+}
+
+static int spel_init_system(struct spel_system *sp, struct device *dev)
+{
+ int i, ret;
+
+ /* Read unit configuration (common for all domains) */
+ spel_update_unit(sp);
+
+ sp->domains = kcalloc(ARRAY_SIZE(domain_info),
+ sizeof(struct spel_domain), GFP_KERNEL);
+ if (!sp->domains)
+ return -ENOMEM;
+
+ spel_init_domains(sp);
+
+ for (i = 0; i < ARRAY_SIZE(domain_info); i++) {
+ struct spel_domain *sd = &sp->domains[i];
+
+ ret = devm_mutex_init(dev, &sd->lock);
+ if (ret) {
+ dev_err(dev, "Failed to initialize mutex for domain %s\n", sd->name);
+ kfree(sp->domains);
+ return ret;
+ }
+
+ spel_detect_powerlimit(sd);
+ }
+
+ return 0;
+}
+
+static int spel_register_powercap(struct spel_system *sp)
+{
+ struct spel_domain *sd;
+ struct powercap_zone *power_zone;
+ int nr_pl, ret, i;
+
+ /* Register SYS domain as parent zone */
+ sd = &sp->domains[SPEL_DOMAIN_SYS];
+ nr_pl = spel_find_nr_power_limit(sd);
+
+ power_zone = powercap_register_zone(&sd->power_zone,
+ sp->control_type, sd->name,
+ NULL, &zone_ops, nr_pl,
+ &constraint_ops);
+ if (IS_ERR(power_zone)) {
+ dev_err(sp->dev, "Failed to register power zone %s\n",
+ sd->name);
+ return PTR_ERR(power_zone);
+ }
+ sp->power_zone = power_zone;
+
+ /* Register other domains as children */
+ for (i = 0; i < ARRAY_SIZE(domain_info); i++) {
+ struct powercap_zone *parent;
+
+ if (i == SPEL_DOMAIN_SYS)
+ continue;
+
+ sd = &sp->domains[i];
+
+ /* SOC is child of SYS, others are children of SOC */
+ if (i == SPEL_DOMAIN_SOC)
+ parent = sp->power_zone;
+ else
+ parent = &sp->domains[SPEL_DOMAIN_SOC].power_zone;
+
+ nr_pl = spel_find_nr_power_limit(sd);
+ power_zone = powercap_register_zone(&sd->power_zone,
+ sp->control_type,
+ sd->name, parent,
+ &zone_ops, nr_pl,
+ &constraint_ops);
+
+ if (IS_ERR(power_zone)) {
+ dev_err(sp->dev, "Failed to register power_zone %s\n",
+ sd->name);
+ ret = PTR_ERR(power_zone);
+ goto err_cleanup;
+ }
+ }
+
+ return 0;
+
+err_cleanup:
+ /* Unregister in reverse order: children first, then SOC, then SYS */
+ for (i = i - 1; i >= 0; i--)
+ powercap_unregister_zone(sp->control_type, &sp->domains[i].power_zone);
+
+ return ret;
+}
+
+static int spel_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct spel_system *sp;
+ int ret;
+
+ sp = devm_kzalloc(dev, sizeof(*sp), GFP_KERNEL);
+ if (!sp)
+ return -ENOMEM;
+
+ sp->dev = dev;
+
+ /* Map config registers (units, capabilities) */
+ sp->config_base = devm_platform_ioremap_resource_byname(pdev, "config");
+ if (IS_ERR(sp->config_base))
+ return PTR_ERR(sp->config_base);
+
+ /* Map constraint registers (power limits) */
+ sp->constraint_base = devm_platform_ioremap_resource_byname(pdev, "constraints");
+ if (IS_ERR(sp->constraint_base))
+ return PTR_ERR(sp->constraint_base);
+
+ /* Map spel domain registers (energy counters) */
+ sp->node_base = devm_platform_ioremap_resource_byname(pdev, "nodes");
+ if (IS_ERR(sp->node_base))
+ return PTR_ERR(sp->node_base);
+
+ sp->control_type = powercap_register_control_type(NULL, "qcom-spel",
+ NULL);
+ if (IS_ERR(sp->control_type)) {
+ dev_err(dev, "Failed to register control type\n");
+ return PTR_ERR(sp->control_type);
+ }
+
+ /* Initialize system and domains */
+ ret = spel_init_system(sp, dev);
+ if (ret) {
+ dev_err(dev, "Failed to initialize system\n");
+ goto err_unregister_control;
+ }
+
+ ret = spel_register_powercap(sp);
+ if (ret) {
+ dev_err(dev, "Failed to register powercap zones\n");
+ if (!sp->power_zone)
+ kfree(sp->domains);
+ goto err_unregister_control;
+ }
+
+ platform_set_drvdata(pdev, sp);
+
+ return 0;
+
+err_unregister_control:
+ powercap_unregister_control_type(sp->control_type);
+ return ret;
+}
+
+static void spel_remove(struct platform_device *pdev)
+{
+ struct spel_system *sp = platform_get_drvdata(pdev);
+ int i;
+
+ /* Unregister in reverse order: children first, then SOC, then SYS */
+ for (i = ARRAY_SIZE(domain_info) - 1; i >= 0; i--)
+ powercap_unregister_zone(sp->control_type, &sp->domains[i].power_zone);
+
+ powercap_unregister_control_type(sp->control_type);
+}
+
+static const struct of_device_id spel_of_match[] = {
+ { .compatible = "qcom,glymur-spel" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, spel_of_match);
+
+static struct platform_driver spel_driver = {
+ .probe = spel_probe,
+ .remove = spel_remove,
+ .driver = {
+ .name = "qcom_spel",
+ .of_match_table = spel_of_match,
+ },
+};
+
+module_platform_driver(spel_driver);
+
+MODULE_DESCRIPTION("Qualcomm SPEL Powercap Driver");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v2 3/3] arm64: dts: qcom: glymur: Enable SPEL powercap driver
2026-06-19 20:39 [PATCH v2 0/3] Add Qualcomm SPEL powercap driver Manaf Meethalavalappu Pallikunhi
2026-06-19 20:39 ` [PATCH v2 1/3] dt-bindings: power: limits: Describe Qualcomm SPEL hardware Manaf Meethalavalappu Pallikunhi
2026-06-19 20:39 ` [PATCH v2 2/3] powercap: qcom: Add SPEL powercap driver Manaf Meethalavalappu Pallikunhi
@ 2026-06-19 20:39 ` Manaf Meethalavalappu Pallikunhi
2 siblings, 0 replies; 5+ messages in thread
From: Manaf Meethalavalappu Pallikunhi @ 2026-06-19 20:39 UTC (permalink / raw)
To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Rafael J. Wysocki,
Bjorn Andersson, Konrad Dybcio, Daniel Lezcano
Cc: Gaurav Kohli, linux-arm-msm, devicetree, linux-kernel, linux-pm,
Manaf Meethalavalappu Pallikunhi
The Qualcomm SoC Power and Electrical Limits (SPEL) provides hardware
based power monitoring and limiting capabilities for various power
domains including System, SoC, CPU clusters, GPU, and various other
subsystems for glymur.
Signed-off-by: Manaf Meethalavalappu Pallikunhi <manaf.pallikunhi@oss.qualcomm.com>
---
arch/arm64/boot/dts/qcom/glymur.dtsi | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/arch/arm64/boot/dts/qcom/glymur.dtsi b/arch/arm64/boot/dts/qcom/glymur.dtsi
index 72c7dc6e4f09..9cf9180e4f8f 100644
--- a/arch/arm64/boot/dts/qcom/glymur.dtsi
+++ b/arch/arm64/boot/dts/qcom/glymur.dtsi
@@ -5033,6 +5033,16 @@ spmi_bus2: spmi@c48000 {
};
};
+ power-limits@ef3b000 {
+ compatible = "qcom,glymur-spel";
+ reg = <0x0 0x0ef3b000 0x0 0x1000>,
+ <0x0 0x0ef3d000 0x0 0x1000>,
+ <0x0 0x0ef3e000 0x0 0x1000>;
+ reg-names = "config",
+ "constraints",
+ "nodes";
+ };
+
tlmm: pinctrl@f100000 {
compatible = "qcom,glymur-tlmm";
reg = <0x0 0x0f100000 0x0 0xf00000>;
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH v2 2/3] powercap: qcom: Add SPEL powercap driver
2026-06-19 20:39 ` [PATCH v2 2/3] powercap: qcom: Add SPEL powercap driver Manaf Meethalavalappu Pallikunhi
@ 2026-06-19 20:50 ` sashiko-bot
0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-06-19 20:50 UTC (permalink / raw)
To: Manaf Meethalavalappu Pallikunhi; +Cc: devicetree, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] The devm-allocated `sp` structure is dereferenced in the asynchronous `spel_release_zone` callback, causing a Use-After-Free when the driver is unbound.
- [High] Manual kfree of the dynamically allocated `sp->domains` array leads to a Use-After-Free when devres attempts to run `mutex_destroy` on its embedded devm-managed mutexes.
- [Medium] The driver incorrectly exposes phantom powercap constraints for domains lacking hardware support, breaking UAPI expectations.
- [Medium] The formula used to calculate `sp->time_unit` contradicts the driver's own documentation and standard scaling logic, resulting in incorrect time unit conversion.
--
commit 53ea37bb963bb111e31263f8aaab73464a037f7d
Author: Manaf Meethalavalappu Pallikunhi <manaf.pallikunhi@oss.qualcomm.com>
Subject: powercap: qcom: Add SPEL powercap driver
This commit adds support for the Qualcomm SoC Power and Electrical Limits
(SPEL) hardware. It enables power limits to be enforced and monitored on
Qualcomm SoCs across multiple domains using the powercap framework.
> diff --git a/drivers/powercap/qcom_spel.c b/drivers/powercap/qcom_spel.c
> new file mode 100644
> index 0000000000000..4dd91cf36ccca
> --- /dev/null
> +++ b/drivers/powercap/qcom_spel.c
[ ... ]
> +static int spel_release_zone(struct powercap_zone *power_zone)
> +{
> + struct spel_domain *sd = power_zone_to_spel_domain(power_zone);
> + struct spel_system *sp = sd->sp;
> +
> + /* Free the domains array when the last zone (SYS domain) is released */
> + if (sd->id == SPEL_DOMAIN_SYS)
> + kfree(sp->domains);
[Severity: High]
If the device is unbound while a sysfs file is held open, devres will free
the devm_kzalloc allocated sp structure immediately. When the user later
closes the sysfs file, the powercap framework will asynchronously call this
release function. Does accessing sp->domains here cause a use-after-free
read?
[Severity: High]
Also, earlier in spel_init_system(), devm_mutex_init() is called on sd->lock
which is embedded inside this dynamically allocated sp->domains array:
spel_init_system() {
...
ret = devm_mutex_init(dev, &sd->lock);
If sp->domains is manually freed here with kfree(), will devres trigger a
use-after-free write when it later runs mutex_destroy() on the embedded
locks during device removal or probe failure?
[ ... ]
> +static void spel_init_domains(struct spel_system *sp)
> +{
> + unsigned int i;
> +
> + for (i = 0; i < ARRAY_SIZE(domain_info); i++) {
> + struct spel_domain *sd = &sp->domains[i];
> +
> + sd->sp = sp;
> + snprintf(sd->name, SPEL_DOMAIN_NAME_LENGTH, "%s",
> + domain_info[i].name);
> + sd->id = i;
> + sd->status_reg = sp->node_base + domain_info[i].offset;
> +
> + /* PL1 is always supported (required for powercap registration) */
> + sp->limits[i] = BIT(POWER_LIMIT1);
[Severity: Medium]
Is it strictly required for all domains to have PL1 enabled?
The powercap framework supports zones with zero constraints for purely
monitoring purposes. Since hardware limits are only defined for the SYS and
SOC domains in the constraints array, unconditionally setting PL1 for other
domains (like CL0 or IGPU) causes constraint sysfs files to be created that
will always fail with an error when read or written. Does this expose a
broken UAPI?
> + }
> +}
> +
> +static void spel_update_unit(struct spel_system *sp)
> +{
> + u32 value, shift;
> +
> + /* Read power_unit and time_unit from offset 0x0 */
> + value = readl(sp->config_base);
> +
> + /*
> + * Unit calculation: 1 / (2^shift)
> + * Masks limit: TIME_UNIT (4 bits, max 15), POWER_UNIT (3 bits, max 7).
> + */
> + shift = FIELD_GET(POWER_UNIT_MASK, value);
> + sp->power_unit = 1000000 / (1 << shift);
> +
> + shift = FIELD_GET(TIME_UNIT_MASK, value);
> + /*
> + * Time window in register is in milliseconds.
> + */
> + sp->time_unit = 1000 * (1 << shift);
[Severity: Medium]
The comment above states the unit calculation is 1 / (2^shift).
For the power unit, the driver correctly divides. However, for the time unit
it multiplies by the shift value instead. Should this be a division
(1000 / (1 << shift)) to correctly scale the time unit into microseconds?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260620-qcom_spel_driver_upstream-v2-0-a3ee6837c18f@oss.qualcomm.com?part=2
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-06-19 20:50 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-19 20:39 [PATCH v2 0/3] Add Qualcomm SPEL powercap driver Manaf Meethalavalappu Pallikunhi
2026-06-19 20:39 ` [PATCH v2 1/3] dt-bindings: power: limits: Describe Qualcomm SPEL hardware Manaf Meethalavalappu Pallikunhi
2026-06-19 20:39 ` [PATCH v2 2/3] powercap: qcom: Add SPEL powercap driver Manaf Meethalavalappu Pallikunhi
2026-06-19 20:50 ` sashiko-bot
2026-06-19 20:39 ` [PATCH v2 3/3] arm64: dts: qcom: glymur: Enable " Manaf Meethalavalappu Pallikunhi
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.