blob: 543885b4229dc395b34c776dae9ab4b2d559d8fd [file] [log] [blame]
// 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_CLIENT10_OFFSET 0x3000
#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;
if (client_id >= 10)
client_offset += CXIP_CLIENT10_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);