Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 35240c59 authored by Rajesh Kemisetti's avatar Rajesh Kemisetti Committed by Deepak Kumar
Browse files

soc: qcom: Add support for Cx iPeak limit driver



Implement common driver to limit Cx ipeak based on
voting from various clients in multimedia.

Change-Id: Ie0a57e49f7a8ba8a4fa3aa7f50dd0947f8e9d11b
Signed-off-by: default avatarRajesh Kemisetti <rajeshk@codeaurora.org>
Signed-off-by: default avatarVikash Garodia <vgarodia@codeaurora.org>
Signed-off-by: default avatarDeepak Kumar <dkumar@codeaurora.org>
parent eb45ec3a
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -819,6 +819,14 @@ config QCOM_QHEE_ENABLE_MEM_PROTECTION
	  This is part of a security feature in QHEE and need to be enabled by
	  default.

config QCOM_CX_IPEAK
	bool "Common driver to handle Cx iPeak limitation"
	help
	  Cx ipeak HW module is used to limit the current drawn by various
	  subsystem blocks on Cx power rail. Each client needs to set their
	  bit in tcsr register if it is going to cross its own threshold.
	  If all clients are going to cross their thresholds then Cx ipeak
	  hw module will raise an interrupt to cDSP block to throttle cDSP fmax.
endmenu

config QCOM_HYP_CORE_CTL
+1 −0
Original line number Diff line number Diff line
@@ -96,3 +96,4 @@ obj-$(CONFIG_QCOM_CDSP_RM) += cdsprm.o
obj-$(CONFIG_ICNSS) += icnss.o
obj-$(CONFIG_ICNSS_QMI) += icnss_qmi.o wlan_firmware_service_v01.o
obj-$(CONFIG_RMNET_CTL) += rmnet_ctl/
obj-$(CONFIG_QCOM_CX_IPEAK) += cx_ipeak.o
+319 −0
Original line number Diff line number Diff line
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) 2018-2019, The Linux Foundation. All rights reserved.
 */

#include <linux/module.h>
#include <linux/io.h>
#include <linux/iopoll.h>
#include <linux/printk.h>
#include <linux/spinlock.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/slab.h>
#include <linux/err.h>

#include <soc/qcom/cx_ipeak.h>

#define TCSR_CXIP_LM_VOTE_FEATURE_ENABLE_OFFSET		0x10

/* v1 register set */
#define TCSR_CXIP_LM_VOTE_BYPASS_OFFSET			0x4
#define TCSR_CXIP_LM_VOTE_CLEAR_OFFSET			0x8
#define TCSR_CXIP_LM_VOTE_SET_OFFSET			0xC
#define TCSR_CXIP_LM_TRS_OFFSET				0x24

/* v2 register set */
#define TCSR_CXIP_LM_VOTE_CLIENTx_BYPASS_OFFSET		0x4
#define TCSR_CXIP_LM_DANGER_OFFSET			0x24

#define CXIP_CLIENT_OFFSET				0x1000
#define CXIP_POLL_TIMEOUT_US (50 * 1000)

struct cx_ipeak_client;

struct cx_ipeak_core_ops {
	int (*update)(struct cx_ipeak_client *client, bool vote);
	struct cx_ipeak_client* (*register_client)(int client_id);
};

static struct cx_ipeak_device {
	spinlock_t vote_lock;
	void __iomem *tcsr_vptr;
	struct cx_ipeak_core_ops *core_ops;
} device_ipeak;

struct cx_ipeak_client {
	int vote_count;
	unsigned int offset;
	struct cx_ipeak_device *dev;
};

/**
 * cx_ipeak_register() - allocate client structure and fill device private and
 *			offset details.
 * @dev_node: device node of the client
 * @client_name: property name of the client
 *
 * Allocate client memory and fill the structure with device private and bit
 *
 */
struct cx_ipeak_client *cx_ipeak_register(struct device_node *dev_node,
		const char *client_name)
{
	struct of_phandle_args cx_spec;
	struct cx_ipeak_client *client = NULL;
	int ret;

	ret = of_parse_phandle_with_fixed_args(dev_node, client_name,
			1, 0, &cx_spec);
	if (ret)
		return ERR_PTR(-EINVAL);

	if (!of_device_is_available(cx_spec.np))
		return NULL;

	if (device_ipeak.tcsr_vptr == NULL)
		return ERR_PTR(-EPROBE_DEFER);

	if (cx_spec.args[0] > 31)
		return ERR_PTR(-EINVAL);

	if (device_ipeak.core_ops)
		client =  device_ipeak.core_ops->register_client
						(cx_spec.args[0]);
	return client;
}
EXPORT_SYMBOL(cx_ipeak_register);

static struct cx_ipeak_client *cx_ipeak_register_v1(int client_id)
{
	struct cx_ipeak_client *client;
	unsigned int reg_enable, reg_bypass;
	void __iomem *vptr = device_ipeak.tcsr_vptr;

	reg_enable = readl_relaxed(device_ipeak.tcsr_vptr +
			TCSR_CXIP_LM_VOTE_FEATURE_ENABLE_OFFSET);
	reg_bypass = readl_relaxed(vptr +
			TCSR_CXIP_LM_VOTE_BYPASS_OFFSET) &
			BIT(client_id);
	if (!reg_enable || reg_bypass)
		return NULL;

	client = kzalloc(sizeof(*client), GFP_KERNEL);
	if (!client)
		return ERR_PTR(-ENOMEM);

	client->offset = BIT(client_id);
	client->dev = &device_ipeak;

	return client;
}

static struct cx_ipeak_client *cx_ipeak_register_v2(int client_id)
{
	unsigned int reg_bypass, reg_enable;
	struct cx_ipeak_client *client;
	unsigned int client_offset = 0;
	void __iomem *vptr = device_ipeak.tcsr_vptr;
	int i;

	for (i = 0; i <= client_id; i++)
		client_offset += CXIP_CLIENT_OFFSET;

	reg_enable = readl_relaxed(device_ipeak.tcsr_vptr +
			TCSR_CXIP_LM_VOTE_FEATURE_ENABLE_OFFSET);
	reg_bypass = readl_relaxed(vptr + client_offset +
			TCSR_CXIP_LM_VOTE_CLIENTx_BYPASS_OFFSET) &
			BIT(0);

	if (!reg_enable || reg_bypass)
		return NULL;

	client = kzalloc(sizeof(*client), GFP_KERNEL);
	if (!client)
		return ERR_PTR(-ENOMEM);

	client->offset = client_offset;
	client->dev = &device_ipeak;

	return client;
}

/*
 * cx_ipeak_update() - Set/Clear client vote for Cx iPeak limit
 * manager to throttle cDSP.
 * @client: client handle.
 * @vote: True to set the vote and False for reset.
 *
 * Receives vote from each client and decides whether to throttle cDSP or not.
 * This function is NOP for the targets which does not support TCSR Cx iPeak.
 */
int cx_ipeak_update(struct cx_ipeak_client *client, bool vote)
{
	/* Check for client and device availability and proceed */
	if (!client)
		return 0;

	if (!client->dev || !client->dev->core_ops || !client->dev->tcsr_vptr)
		return -EINVAL;

	return client->dev->core_ops->update(client, vote);
}
EXPORT_SYMBOL(cx_ipeak_update);

static int cx_ipeak_update_v1(struct cx_ipeak_client *client, bool vote)
{
	unsigned int reg_val;
	int ret = 0;

	spin_lock(&client->dev->vote_lock);

	if (vote) {
		if (client->vote_count == 0) {
			writel_relaxed(client->offset,
				       client->dev->tcsr_vptr +
				       TCSR_CXIP_LM_VOTE_SET_OFFSET);
			/*
			 * Do a dummy read to give enough time for TRS register
			 * to become 1 when the last client votes.
			 */
			readl_relaxed(client->dev->tcsr_vptr +
				      TCSR_CXIP_LM_TRS_OFFSET);

			ret = readl_poll_timeout(client->dev->tcsr_vptr +
						 TCSR_CXIP_LM_TRS_OFFSET,
						 reg_val, !reg_val, 0,
						 CXIP_POLL_TIMEOUT_US);
			if (ret) {
				writel_relaxed(client->offset,
					       client->dev->tcsr_vptr +
					       TCSR_CXIP_LM_VOTE_CLEAR_OFFSET);
				goto done;
			}
		}
		client->vote_count++;
	} else {
		if (client->vote_count > 0) {
			client->vote_count--;
			if (client->vote_count == 0) {
				writel_relaxed(client->offset,
					       client->dev->tcsr_vptr +
					       TCSR_CXIP_LM_VOTE_CLEAR_OFFSET);
			}
		} else
			ret = -EINVAL;
	}

done:
	spin_unlock(&client->dev->vote_lock);
	return ret;
}

static int cx_ipeak_update_v2(struct cx_ipeak_client *client, bool vote)
{
	unsigned int reg_val;
	int ret = 0;

	spin_lock(&client->dev->vote_lock);

	if (vote) {
		if (client->vote_count == 0) {
			writel_relaxed(BIT(0),
				       client->dev->tcsr_vptr +
				       client->offset);

			ret = readl_poll_timeout(client->dev->tcsr_vptr +
						 TCSR_CXIP_LM_DANGER_OFFSET,
						 reg_val, !reg_val, 0,
						 CXIP_POLL_TIMEOUT_US);
			if (ret) {
				writel_relaxed(0,
					       client->dev->tcsr_vptr +
					       client->offset);
				goto done;
			}
		}
		client->vote_count++;
	} else {
		if (client->vote_count > 0) {
			client->vote_count--;
			if (client->vote_count == 0) {
				writel_relaxed(0,
					       client->dev->tcsr_vptr +
					       client->offset);
			}
		} else {
			ret = -EINVAL;
		}
	}

done:
	spin_unlock(&client->dev->vote_lock);
	return ret;
}

/**
 * cx_ipeak_unregister() - unregister client
 * @client: client address to free
 *
 * Free the client memory
 */
void cx_ipeak_unregister(struct cx_ipeak_client *client)
{
	kfree(client);
}
EXPORT_SYMBOL(cx_ipeak_unregister);

struct cx_ipeak_core_ops core_ops_v1 = {
	.update = cx_ipeak_update_v1,
	.register_client = cx_ipeak_register_v1,
};

struct cx_ipeak_core_ops core_ops_v2 = {
	.update = cx_ipeak_update_v2,
	.register_client = cx_ipeak_register_v2,
};

static int cx_ipeak_probe(struct platform_device *pdev)
{
	struct resource *res;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	device_ipeak.tcsr_vptr = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(device_ipeak.tcsr_vptr))
		return PTR_ERR(device_ipeak.tcsr_vptr);

	if (of_device_is_compatible(pdev->dev.of_node, "qcom,cx-ipeak-v1"))
		device_ipeak.core_ops = &core_ops_v1;
	else if (of_device_is_compatible(pdev->dev.of_node,
					 "qcom,cx-ipeak-v2"))
		device_ipeak.core_ops = &core_ops_v2;
	else
		device_ipeak.core_ops = NULL;

	spin_lock_init(&device_ipeak.vote_lock);
	return 0;
}

static const struct of_device_id cx_ipeak_match_table[] = {
	{ .compatible = "qcom,cx-ipeak-v1"},
	{ .compatible = "qcom,cx-ipeak-v2"},
	{}
};

static struct platform_driver cx_ipeak_platform_driver = {
	.probe = cx_ipeak_probe,
	.driver = {
		.name  = "cx_ipeak",
		.of_match_table = cx_ipeak_match_table,
		.suppress_bind_attrs = true,
	}
};

static int __init cx_ipeak_init(void)
{
	return platform_driver_register(&cx_ipeak_platform_driver);
}

arch_initcall(cx_ipeak_init);