msm: clock: Move to per-clock voltage class support in the top-level driver
Generalize the voltage voting logic so that it lives in the
top-level clock.c driver. This has a couple of advantages:
- Voltage voting is no longer restricted to clock-local drivers
and need not be duplicated for different driver types.
- Different clocks may specify requirements on different power rails
by using different voltage_class implementations.
- Fmax data (maximum frequency allowed at a given voltage level)
is separated from the frequency tables and captured as a
property of each clock, so that difference SoCs may share the
same frequency tables even if their voltage requirements differ.
- The per-clock lock can be managed entirely at the clock.c level
Change-Id: I2bbd16096c14cedefa2b7fdf77d9f20d842b1e4d
Signed-off-by: Matt Wagantall <mattw@codeaurora.org>
diff --git a/arch/arm/mach-msm/clock.c b/arch/arm/mach-msm/clock.c
index 58a64a3..de89382 100644
--- a/arch/arm/mach-msm/clock.c
+++ b/arch/arm/mach-msm/clock.c
@@ -24,13 +24,116 @@
#include "clock.h"
+/* Find the voltage level required for a given rate. */
+static int find_vdd_level(struct clk *clk, unsigned long rate)
+{
+ int level;
+
+ for (level = 0; level < ARRAY_SIZE(clk->fmax); level++)
+ if (rate <= clk->fmax[level])
+ break;
+
+ if (level == ARRAY_SIZE(clk->fmax)) {
+ pr_err("Rate %lu for %s is greater than highest Fmax\n", rate,
+ clk->dbg_name);
+ return -EINVAL;
+ }
+
+ return level;
+}
+
+/* Update voltage level given the current votes. */
+static int update_vdd(struct clk_vdd_class *vdd_class)
+{
+ int level, rc;
+
+ for (level = ARRAY_SIZE(vdd_class->level_votes)-1; level > 0; level--)
+ if (vdd_class->level_votes[level])
+ break;
+
+ if (level == vdd_class->cur_level)
+ return 0;
+
+ rc = vdd_class->set_vdd(vdd_class, level);
+ if (!rc)
+ vdd_class->cur_level = level;
+
+ return rc;
+}
+
+/* Vote for a voltage level. */
+int vote_vdd_level(struct clk_vdd_class *vdd_class, int level)
+{
+ unsigned long flags;
+ int rc;
+
+ spin_lock_irqsave(&vdd_class->lock, flags);
+ vdd_class->level_votes[level]++;
+ rc = update_vdd(vdd_class);
+ if (rc)
+ vdd_class->level_votes[level]--;
+ spin_unlock_irqrestore(&vdd_class->lock, flags);
+
+ return rc;
+}
+
+/* Remove vote for a voltage level. */
+int unvote_vdd_level(struct clk_vdd_class *vdd_class, int level)
+{
+ unsigned long flags;
+ int rc = 0;
+
+ spin_lock_irqsave(&vdd_class->lock, flags);
+ if (WARN(!vdd_class->level_votes[level],
+ "Reference counts are incorrect for %s level %d\n",
+ vdd_class->class_name, level))
+ goto out;
+ vdd_class->level_votes[level]--;
+ rc = update_vdd(vdd_class);
+ if (rc)
+ vdd_class->level_votes[level]++;
+out:
+ spin_unlock_irqrestore(&vdd_class->lock, flags);
+ return rc;
+}
+
+/* Vote for a voltage level corresponding to a clock's rate. */
+static int vote_rate_vdd(struct clk *clk, unsigned long rate)
+{
+ int level;
+
+ if (!clk->vdd_class)
+ return 0;
+
+ level = find_vdd_level(clk, rate);
+ if (level < 0)
+ return level;
+
+ return vote_vdd_level(clk->vdd_class, level);
+}
+
+/* Remove vote for a voltage level corresponding to a clock's rate. */
+static void unvote_rate_vdd(struct clk *clk, unsigned long rate)
+{
+ int level;
+
+ if (!clk->vdd_class)
+ return;
+
+ level = find_vdd_level(clk, rate);
+ if (level < 0)
+ return;
+
+ unvote_vdd_level(clk->vdd_class, level);
+}
+
/*
* Standard clock functions defined in include/linux/clk.h
*/
int clk_enable(struct clk *clk)
{
int ret = 0;
- unsigned long flags;
+ unsigned long flags, rate;
struct clk *parent;
if (!clk)
@@ -39,22 +142,22 @@
spin_lock_irqsave(&clk->lock, flags);
if (clk->count == 0) {
parent = clk_get_parent(clk);
+ rate = clk_get_rate(clk);
+
ret = clk_enable(parent);
if (ret)
- goto out;
+ goto err_enable_parent;
ret = clk_enable(clk->depends);
- if (ret) {
- clk_disable(parent);
- goto out;
- }
+ if (ret)
+ goto err_enable_depends;
+ ret = vote_rate_vdd(clk, rate);
+ if (ret)
+ goto err_vote_vdd;
if (clk->ops->enable)
ret = clk->ops->enable(clk);
- if (ret) {
- clk_disable(clk->depends);
- clk_disable(parent);
- goto out;
- }
+ if (ret)
+ goto err_enable_clock;
} else if (clk->flags & CLKFLAG_HANDOFF_RATE) {
/*
* The clock was already enabled by handoff code so there is no
@@ -69,6 +172,16 @@
out:
spin_unlock_irqrestore(&clk->lock, flags);
+ return 0;
+
+err_enable_clock:
+ unvote_rate_vdd(clk, rate);
+err_vote_vdd:
+ clk_disable(clk->depends);
+err_enable_depends:
+ clk_disable(parent);
+err_enable_parent:
+ spin_unlock_irqrestore(&clk->lock, flags);
return ret;
}
EXPORT_SYMBOL(clk_enable);
@@ -76,7 +189,6 @@
void clk_disable(struct clk *clk)
{
unsigned long flags;
- struct clk *parent;
if (!clk)
return;
@@ -85,10 +197,13 @@
if (WARN(clk->count == 0, "%s is unbalanced", clk->dbg_name))
goto out;
if (clk->count == 1) {
+ struct clk *parent = clk_get_parent(clk);
+ unsigned long rate = clk_get_rate(clk);
+
if (clk->ops->disable)
clk->ops->disable(clk);
+ unvote_rate_vdd(clk, rate);
clk_disable(clk->depends);
- parent = clk_get_parent(clk);
clk_disable(parent);
}
clk->count--;
@@ -117,10 +232,35 @@
int clk_set_rate(struct clk *clk, unsigned long rate)
{
+ unsigned long start_rate, flags;
+ int rc;
+
if (!clk->ops->set_rate)
return -ENOSYS;
- return clk->ops->set_rate(clk, rate);
+ spin_lock_irqsave(&clk->lock, flags);
+ if (clk->count) {
+ start_rate = clk_get_rate(clk);
+ /* Enforce vdd requirements for target frequency. */
+ rc = vote_rate_vdd(clk, rate);
+ if (rc)
+ goto err_vote_vdd;
+ rc = clk->ops->set_rate(clk, rate);
+ if (rc)
+ goto err_set_rate;
+ /* Release vdd requirements for starting frequency. */
+ unvote_rate_vdd(clk, start_rate);
+ } else {
+ rc = clk->ops->set_rate(clk, rate);
+ }
+ spin_unlock_irqrestore(&clk->lock, flags);
+ return rc;
+
+err_set_rate:
+ unvote_rate_vdd(clk, rate);
+err_vote_vdd:
+ spin_unlock_irqrestore(&clk->lock, flags);
+ return rc;
}
EXPORT_SYMBOL(clk_set_rate);