Devicetree
 help / color / mirror / Atom feed
From: Daniel Lezcano <daniel.lezcano@oss.qualcomm.com>
To: Manaf Meethalavalappu Pallikunhi
	<manaf.pallikunhi@oss.qualcomm.com>,
	Rob Herring <robh@kernel.org>,
	Krzysztof Kozlowski <krzk+dt@kernel.org>,
	Conor Dooley <conor+dt@kernel.org>,
	"Rafael J. Wysocki" <rafael@kernel.org>,
	Bjorn Andersson <andersson@kernel.org>,
	Konrad Dybcio <konradybcio@kernel.org>
Cc: Gaurav Kohli <gaurav.kohli@oss.qualcomm.com>,
	linux-arm-msm@vger.kernel.org, devicetree@vger.kernel.org,
	linux-kernel@vger.kernel.org, linux-pm@vger.kernel.org
Subject: Re: [PATCH v3 3/4] powercap: qcom: Add SPEL powercap driver
Date: Mon, 6 Jul 2026 15:52:56 +0200	[thread overview]
Message-ID: <92397fd7-e149-432c-9ec8-b2cd31af7f04@oss.qualcomm.com> (raw)
In-Reply-To: <20260702-qcom_spel_driver_upstream-v3-3-434d50f0c5b0@oss.qualcomm.com>


Hi Manaf,

I've commented this patch but I've more comments which should be 
addressed after taking into account the ones below.

On 7/2/26 19:22, Manaf Meethalavalappu Pallikunhi wrote:
> 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.

Could you elaborate a bit more the description in order to make easier 
the understanding of the code ?

> 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 | 803 +++++++++++++++++++++++++++++++++++++++++++
>   4 files changed, 818 insertions(+)
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 44e90b2d5e85..5e716662782c 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -22374,6 +22374,7 @@ L:	linux-arm-msm@vger.kernel.org
>   S:	Maintained
>   F:	Documentation/devicetree/bindings/power/limits/power-limit-controller.yaml
>   F:	Documentation/devicetree/bindings/power/limits/qcom,glymur-spel.yaml
> +F:	drivers/powercap/qcom_spel.c
>   
>   QUALCOMM TEE (QCOMTEE) DRIVER

[ ... ]

> +#define ENERGY_UNIT_SCALE		1000

You should have already a define for that in <linux/units.h>

> +#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",
> +};
> +
> +/* Common domain names (can be shared across SoCs) */
> +static const char * const spel_domain_names[SPEL_DOMAIN_MAX] = {
> +	[SPEL_DOMAIN_SYS]	= "sys",
> +	[SPEL_DOMAIN_SOC]	= "soc",
> +	[SPEL_DOMAIN_CL0]	= "cl0",
> +	[SPEL_DOMAIN_CL1]	= "cl1",
> +	[SPEL_DOMAIN_CL2]	= "cl2",
> +	[SPEL_DOMAIN_IGPU]	= "igpu",
> +	[SPEL_DOMAIN_DGPU]	= "dgpu",
> +	[SPEL_DOMAIN_NSP]	= "nsp",
> +	[SPEL_DOMAIN_MMCX]	= "mmcx",
> +	[SPEL_DOMAIN_INFRA]	= "infra",
> +	[SPEL_DOMAIN_DRAM]	= "dram",
> +	[SPEL_DOMAIN_MDM]	= "mdm",
> +	[SPEL_DOMAIN_WLAN]	= "wlan",
> +	[SPEL_DOMAIN_USB1]	= "usb1",
> +	[SPEL_DOMAIN_USB2]	= "usb2",
> +	[SPEL_DOMAIN_USB3]	= "usb3",
> +};
> +
> +/* Glymur-specific domain register offsets */
> +static const u32 glymur_domain_offsets[SPEL_DOMAIN_MAX] = {
> +	[SPEL_DOMAIN_SYS]	= 0x40,
> +	[SPEL_DOMAIN_SOC]	= 0x00,
> +	[SPEL_DOMAIN_CL0]	= 0x5c,
> +	[SPEL_DOMAIN_CL1]	= 0x60,
> +	[SPEL_DOMAIN_CL2]	= 0x64,
> +	[SPEL_DOMAIN_IGPU]	= 0x08,
> +	[SPEL_DOMAIN_DGPU]	= 0x44,
> +	[SPEL_DOMAIN_NSP]	= 0x0c,
> +	[SPEL_DOMAIN_MMCX]	= 0x10,
> +	[SPEL_DOMAIN_INFRA]	= 0x18,
> +	[SPEL_DOMAIN_DRAM]	= 0x1c,
> +	[SPEL_DOMAIN_MDM]	= 0x48,
> +	[SPEL_DOMAIN_WLAN]	= 0x4c,
> +	[SPEL_DOMAIN_USB1]	= 0x50,
> +	[SPEL_DOMAIN_USB2]	= 0x54,
> +	[SPEL_DOMAIN_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;

It is a cyclic dependency sp<->sd. Please break it.

IMO it is acceptable to have a static global variable struct spel_system.

> +	void __iomem *status_reg;
> +	char name[SPEL_DOMAIN_NAME_LENGTH];
> +	enum spel_domain_type id;
> +};

Instead of having a spel_domain_names[] declared with the enum where the 
name is copied in the name field above, it would be simpler and safer to 
do that with the struct spel_domain itself.

Also add the offset instead of a separate array.

struct spel_domain {
	...
	const char *name;
	...
	unsigned char offset;
};

struct spel_domain spel_domains[] = {
	{ .name = "sys", .offset = 0x40 },
	{ .name = "soc", .offset = 0x00 },
	{ .name = "cl0", .offset = 0x5c },
	{ .name = "cl1", .offset = 0x60 },
	{ .name = "cl2", .offset = 0x64 },
	...
};

#define NUM_SPEL_DOMAINS ARRAY_SIZE(spel_domains)

In addition, I suggest to describe the hierarchy directly when defining 
the structures:

struct spel_domain {
	...
	struct spel_domain *parent;
	...
};

struct spel_domain spel_domains[] = {
	{ .name = "sys", .offset = 0x40 },
	{ .name = "soc", .offset = 0x00, .parent = &spel_domains[0] },
	{ .name = "cl0", .offset = 0x5c, .parent = &spel_domains[1] },
	{ .name = "cl1", .offset = 0x60, .parent = &spel_domains[1] },
	{ .name = "cl2", .offset = 0x64, .parent = &spel_domains[1] },
	...
};

It will facilitate the initialization.

> +/**
> + * 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;

This power_zone is not needed

> +	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];

If the limit is an attribute of the spel_domain instead of an array 
here, the resulting code should be simpler to read and understand.

> +	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)
> +{
> +	for (int 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;
> +
> +		FIELD_MODIFY(POWER_LIMIT_MASK, &reg_val, 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)
> +		 */
> +		FIELD_MODIFY(POWER_LIMIT_ENABLE, &reg_val, new_val ? 1 : 0);
> +
> +		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 */
> +		FIELD_MODIFY(TIME_WINDOW_MASK_H, &reg_val, new_val >> 15);
> +		FIELD_MODIFY(TIME_WINDOW_MASK_L, &reg_val, new_val & FIELD_MAX(TIME_WINDOW_MASK_L));
> +		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);
> +
> +	/*
> +	 * Free the domains array when the last zone (SYS domain) is released.
> +	 * This ensures proper cleanup even if sysfs files are held open during unbind.
> +	 */
> +	if (sd->id == SPEL_DOMAIN_SYS) {
> +		struct spel_domain *domains = sd;
> +
> +		/* Destroy all mutexes before freeing the domains array */
> +		for (int i = 0; i < ARRAY_SIZE(spel_domain_names); i++)
> +			mutex_destroy(&domains[i].lock);
> +
> +		kfree(domains);
> +	}
> +
> +	return 0;
> +}
> +
> +static int spel_find_nr_power_limit(struct spel_domain *sd)
> +{
> +	int nr_pl = 0;
> +
> +	for (int 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,

Is reset_energy_uj() supported ?

I thought the SPEL allowed power monitoring, right ?

What about get_power_uw() / get_max_power_range_uw() ?

> +	.release = spel_release_zone,
> +};
> +
> +static int spel_constraint_to_pl(struct spel_domain *sd, int cid)
> +{
> +	int id = 0;
> +
> +	for (int i = 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(spel_domain_names); i++) {
> +		struct spel_domain *sd = &sp->domains[i];
> +
> +		sd->sp = sp;
> +		snprintf(sd->name, SPEL_DOMAIN_NAME_LENGTH, "%s",
> +			 spel_domain_names[i]);
> +		sd->id = i;
> +		sd->status_reg = sp->node_base + glymur_domain_offsets[i];
> +
> +		/* 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);
> +	/*
> +	 * Convert to microseconds: base unit is 1ms, divided by 2^shift.
> +	 */
> +	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);

May be check power_unit/time_unit/energy_unit before leaving the 
function to make sure they are greater than zero as they are used to 
divide ?

> +	dev_dbg(sp->dev, "Units: energy=%dnJ, time=%dus, power=%duW\n",
> +		sp->energy_unit, sp->time_unit, sp->power_unit);

Check what can be used from units.h for all conversions done in this 
function

> +}
> +
> +static void spel_detect_powerlimit(struct spel_domain *sd)
> +{
> +	struct spel_system *sp = sd->sp;
> +	u32 capabilities;
> +
> +	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 (int i = 1; i < ARRAY_SIZE(pl_names); i++) {
> +		for (int 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)
> +{
> +	/* Read unit configuration (common for all domains) */
> +	spel_update_unit(sp);
> +
> +	sp->domains = kcalloc(ARRAY_SIZE(spel_domain_names),
> +			      sizeof(struct spel_domain), GFP_KERNEL);

	devm_kcalloc() ?

> +	if (!sp->domains)
> +		return -ENOMEM;
> +
> +	spel_init_domains(sp);
> +
> +	for (int i = 0; i < ARRAY_SIZE(spel_domain_names); i++) {
> +		struct spel_domain *sd = &sp->domains[i];
> +
> +		mutex_init(&sd->lock);
> +		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;
> +
> +	/* 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;

Why is this power_zone needed ?

> +
> +	/* Register other domains as children */
> +	for (int i = 0; i < ARRAY_SIZE(spel_domain_names); 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);
> +			/* Unregister in reverse order: children first, then SOC, then SYS */
> +			for (int j = i - 1; j >= 0; j--)
> +				powercap_unregister_zone(sp->control_type,
> +							 &sp->domains[j].power_zone);
> +			return ret;
> +		}
> +	}
 > +
> +	return 0;
> +}
> +
> +static int spel_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	struct powercap_control_type *ct;
> +	struct spel_system *sp;
> +	int ret;
> +
> +	sp = kzalloc_obj(struct spel_system);
> +	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)) {
> +		ret = PTR_ERR(sp->config_base);
> +		goto err_free_sp;
> +	}
> +
> +	/* Map constraint registers (power limits) */
> +	sp->constraint_base = devm_platform_ioremap_resource_byname(pdev, "constraints");
> +	if (IS_ERR(sp->constraint_base)) {
> +		ret = PTR_ERR(sp->constraint_base);
> +		goto err_free_sp;
> +	}
> +
> +	/* Map spel domain registers (energy counters) */
> +	sp->node_base = devm_platform_ioremap_resource_byname(pdev, "nodes");
> +	if (IS_ERR(sp->node_base)) {
> +		ret = PTR_ERR(sp->node_base);
> +		goto err_free_sp;
> +	}
> +
> +	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");
> +		ret = PTR_ERR(sp->control_type);
> +		goto err_free_sp;
> +	}
> +
> +	/* Save control_type before it might be freed by spel_release_zone() */
> +	ct = 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 SYS zone was registered, err_cleanup inside spel_register_powercap
> +		 * already unregistered all zones and spel_release_zone freed sp->domains.
> +		 * If SYS was never registered (sp->power_zone == NULL), free manually.
> +		 */
> +		if (!sp->power_zone)
> +			kfree(sp->domains);
> +		kfree(sp);
> +		powercap_unregister_control_type(ct);
> +		return ret;
> +	}
> +
> +	platform_set_drvdata(pdev, sp);
> +
> +	return 0;
> +
> +err_unregister_control:
> +	powercap_unregister_control_type(ct);
> +err_free_sp:
> +	kfree(sp);
> +	return ret;
> +}
> +
> +static void spel_remove(struct platform_device *pdev)
> +{
> +	struct spel_system *sp = platform_get_drvdata(pdev);
> +	struct powercap_control_type *ct = sp->control_type;
> +
> +	/*
> +	 * Unregister in reverse order: children first, then SOC, then SYS.
> +	 */
> +	for (int i = ARRAY_SIZE(spel_domain_names) - 1; i >= 0; i--)
> +		powercap_unregister_zone(ct, &sp->domains[i].power_zone);
> +
> +	powercap_unregister_control_type(ct);
> +
> +	kfree(sp);
> +}
> +
> +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");
> 


  parent reply	other threads:[~2026-07-06 13:53 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-02 17:22 [PATCH v3 0/4] Add Qualcomm SPEL powercap driver Manaf Meethalavalappu Pallikunhi
2026-07-02 17:22 ` [PATCH v3 1/4] dt-bindings: power: Add common power limit controller schema Manaf Meethalavalappu Pallikunhi
2026-07-02 17:44   ` sashiko-bot
2026-07-03  6:20   ` Krzysztof Kozlowski
2026-07-02 17:22 ` [PATCH v3 2/4] dt-bindings: power: limits: Describe Qualcomm SPEL hardware Manaf Meethalavalappu Pallikunhi
2026-07-02 17:30   ` sashiko-bot
2026-07-03  6:21   ` Krzysztof Kozlowski
2026-07-02 17:22 ` [PATCH v3 3/4] powercap: qcom: Add SPEL powercap driver Manaf Meethalavalappu Pallikunhi
2026-07-02 17:36   ` sashiko-bot
2026-07-03  6:24   ` Krzysztof Kozlowski
2026-07-06 11:57   ` Konrad Dybcio
2026-07-06 13:52   ` Daniel Lezcano [this message]
2026-07-02 17:22 ` [PATCH v3 4/4] arm64: dts: qcom: glymur: Enable " Manaf Meethalavalappu Pallikunhi

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=92397fd7-e149-432c-9ec8-b2cd31af7f04@oss.qualcomm.com \
    --to=daniel.lezcano@oss.qualcomm.com \
    --cc=andersson@kernel.org \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=gaurav.kohli@oss.qualcomm.com \
    --cc=konradybcio@kernel.org \
    --cc=krzk+dt@kernel.org \
    --cc=linux-arm-msm@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-pm@vger.kernel.org \
    --cc=manaf.pallikunhi@oss.qualcomm.com \
    --cc=rafael@kernel.org \
    --cc=robh@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox