[ARM] tegra: clock: Add new dvfs
authorColin Cross <ccross@android.com>
Thu, 9 Sep 2010 02:46:13 +0000 (19:46 -0700)
committerColin Cross <ccross@android.com>
Mon, 25 Oct 2010 21:57:38 +0000 (14:57 -0700)
New and improved dvfs:
Registered dynamically during init
Exports dvfs functions to control clocks that are not visible to
  the clock subsystem
Supports multiple regulators per clock
Fix dvfs on disabled clocks
Adds /d/clock/dvfs to show current voltage requirements

Change-Id: I93794a7761dccc702566e8850bb79f344ff787a2
Signed-off-by: Colin Cross <ccross@android.com>
arch/arm/mach-tegra/Makefile
arch/arm/mach-tegra/clock.c
arch/arm/mach-tegra/clock.h
arch/arm/mach-tegra/dvfs.c [new file with mode: 0644]
arch/arm/mach-tegra/dvfs.h [new file with mode: 0644]
arch/arm/mach-tegra/include/mach/clk.h

index 22e59f357a92846223033b2b8c5e4a0f2dd950d2..7587d245bfebdced5327107c9981b6a936f7bc04 100644 (file)
@@ -2,6 +2,7 @@ obj-y                                   += common.o
 obj-y                                   += io.o
 obj-y                                   += irq.o legacy_irq.o
 obj-y                                   += clock.o
+obj-y                                   += dvfs.o
 obj-y                                   += timer.o
 obj-y                                   += gpio.o
 obj-y                                   += pinmux.o
index cca4a66faa01e7c259c1b51e2ece7bff34c338e1..09b5993c55136af44251ec1e419e1ea054108296 100644 (file)
 #include <linux/slab.h>
 #include <linux/seq_file.h>
 #include <asm/clkdev.h>
+#include <mach/clk.h>
 
 #include "board.h"
 #include "clock.h"
+#include "dvfs.h"
 
 static LIST_HEAD(clocks);
 
+/*
+ * clock_lock must be held when:
+ *   Accessing any clock register non-atomically
+ *     or
+ *   Relying on any state of a clk struct not to change, unless clk_is_dvfs
+ *   returns true on that clk struct, and dvfs_lock is held instead.
+ *
+ *   Any function that changes the state of a clk struct must hold
+ *   the dvfs_lock if clk_is_auto_dvfs(clk) is true, and the clock_lock.
+ *
+ *   When taking dvfs_lock and clock_lock, dvfs_lock must be taken first.
+ */
 static DEFINE_SPINLOCK(clock_lock);
+
+static inline bool clk_is_auto_dvfs(struct clk *c)
+{
+       smp_rmb();
+       return c->auto_dvfs;
+};
+
+static inline bool clk_is_dvfs(struct clk *c)
+{
+       smp_rmb();
+       return c->is_dvfs;
+};
+
 struct clk *tegra_get_clock_by_name(const char *name)
 {
        struct clk *c;
@@ -48,22 +75,31 @@ struct clk *tegra_get_clock_by_name(const char *name)
        return ret;
 }
 
-static void clk_recalculate_rate(struct clk *c)
+static unsigned long clk_predict_rate_from_parent(struct clk *c, struct clk *p)
 {
        u64 rate;
 
-       if (!c->parent)
-               return;
-
-       rate = c->parent->rate;
+       rate = p->rate;
 
        if (c->mul != 0 && c->div != 0) {
                rate = rate * c->mul;
                do_div(rate, c->div);
        }
 
+       return rate;
+}
+
+static void clk_recalculate_rate(struct clk *c)
+{
+       unsigned long rate;
+
+       if (!c->parent)
+               return;
+
+       rate = clk_predict_rate_from_parent(c, c->parent);
+
        if (rate > c->max_rate)
-               pr_warn("clocks: Set clock %s to rate %llu, max is %lu\n",
+               pr_warn("clocks: Set clock %s to rate %lu, max is %lu\n",
                        c->name, rate, c->max_rate);
 
        c->rate = rate;
@@ -95,6 +131,7 @@ void clk_init(struct clk *c)
 
        INIT_LIST_HEAD(&c->children);
        INIT_LIST_HEAD(&c->sibling);
+       INIT_LIST_HEAD(&c->dvfs);
 
        if (c->ops && c->ops->init)
                c->ops->init(c);
@@ -152,10 +189,21 @@ int clk_enable(struct clk *c)
        int ret;
        unsigned long flags;
 
+       if (clk_is_auto_dvfs(c)) {
+               lock_dvfs();
+               ret = tegra_dvfs_set_rate(c, c->rate);
+               if (ret)
+                       goto out;
+       }
+
        spin_lock_irqsave(&clock_lock, flags);
        ret = clk_enable_locked(c);
        spin_unlock_irqrestore(&clock_lock, flags);
 
+out:
+       if (clk_is_auto_dvfs(c))
+               unlock_dvfs();
+
        return ret;
 }
 EXPORT_SYMBOL(clk_enable);
@@ -182,9 +230,18 @@ void clk_disable(struct clk *c)
 {
        unsigned long flags;
 
+       if (clk_is_auto_dvfs(c))
+               lock_dvfs();
+
        spin_lock_irqsave(&clock_lock, flags);
        clk_disable_locked(c);
        spin_unlock_irqrestore(&clock_lock, flags);
+
+       if (clk_is_auto_dvfs(c)) {
+               if (c->refcnt == 0)
+                       tegra_dvfs_set_rate(c, 0);
+               unlock_dvfs();
+       }
 }
 EXPORT_SYMBOL(clk_disable);
 
@@ -209,11 +266,32 @@ int clk_set_parent_locked(struct clk *c, struct clk *parent)
 
 int clk_set_parent(struct clk *c, struct clk *parent)
 {
-       int ret;
+       int ret = 0;
        unsigned long flags;
+       unsigned long new_rate = clk_predict_rate_from_parent(c, parent);
+
+
+       if (clk_is_auto_dvfs(c)) {
+               lock_dvfs();
+               if (c->refcnt > 0 && (!c->parent || new_rate > c->rate))
+                       ret = tegra_dvfs_set_rate(c, new_rate);
+               if (!ret)
+                       goto out;
+       }
+
        spin_lock_irqsave(&clock_lock, flags);
        ret = clk_set_parent_locked(c, parent);
        spin_unlock_irqrestore(&clock_lock, flags);
+       if (!ret)
+               goto out;
+
+       if (clk_is_auto_dvfs(c) && c->refcnt > 0)
+               ret = tegra_dvfs_set_rate(c, new_rate);
+
+out:
+       if (clk_is_auto_dvfs(c))
+               unlock_dvfs();
+
        return ret;
 }
 EXPORT_SYMBOL(clk_set_parent);
@@ -228,12 +306,17 @@ int clk_set_rate_locked(struct clk *c, unsigned long rate)
 {
        int ret;
 
+       if (rate == c->requested_rate)
+               return 0;
+
        if (rate > c->max_rate)
                rate = c->max_rate;
 
        if (!c->ops || !c->ops->set_rate)
                return -ENOSYS;
 
+       c->requested_rate = rate;
+
        ret = c->ops->set_rate(c, rate);
 
        if (ret)
@@ -251,10 +334,27 @@ int clk_set_rate(struct clk *c, unsigned long rate)
        int ret = 0;
        unsigned long flags;
 
+       if (clk_is_auto_dvfs(c)) {
+               lock_dvfs();
+               if (rate > c->rate && c->refcnt > 0)
+                       ret = tegra_dvfs_set_rate(c, rate);
+               if (ret)
+                       goto out;
+       }
+
        spin_lock_irqsave(&clock_lock, flags);
        ret = clk_set_rate_locked(c, rate);
        spin_unlock_irqrestore(&clock_lock, flags);
 
+       if (ret)
+               goto out;
+
+       if (clk_is_auto_dvfs(c) && c->refcnt > 0)
+               ret = tegra_dvfs_set_rate(c, rate);
+
+out:
+       if (clk_is_auto_dvfs(c))
+               unlock_dvfs();
        return ret;
 }
 EXPORT_SYMBOL(clk_set_rate);
@@ -363,12 +463,27 @@ void __init tegra_init_clock(void)
        tegra2_init_clocks();
 }
 
+void __init tegra_clk_set_dvfs_rates(void)
+{
+       struct clk *c;
+       list_for_each_entry(c, &clocks, node) {
+               if (clk_is_auto_dvfs(c)) {
+                       if (c->refcnt > 0)
+                               tegra_dvfs_set_rate(c, c->rate);
+                       else
+                               tegra_dvfs_set_rate(c, 0);
+               } else if (clk_is_dvfs(c)) {
+                       tegra_dvfs_set_rate(c, c->dvfs_rate);
+               }
+       }
+}
+
 int __init tegra_disable_boot_clocks(void)
 {
        unsigned long flags;
-
        struct clk *c;
 
+       lock_dvfs();
        spin_lock_irqsave(&clock_lock, flags);
 
        list_for_each_entry(c, &clocks, node) {
@@ -382,7 +497,7 @@ int __init tegra_disable_boot_clocks(void)
        }
 
        spin_unlock_irqrestore(&clock_lock, flags);
-
+       unlock_dvfs();
        return 0;
 }
 late_initcall(tegra_disable_boot_clocks);
@@ -390,11 +505,20 @@ late_initcall(tegra_disable_boot_clocks);
 #ifdef CONFIG_DEBUG_FS
 static struct dentry *clk_debugfs_root;
 
+static void dvfs_show_one(struct seq_file *s, struct dvfs *d, int level)
+{
+       seq_printf(s, "%*s  %-*s%21s%d mV\n",
+                       level * 3 + 1, "",
+                       30 - level * 3, d->reg_id,
+                       "",
+                       d->cur_millivolts);
+}
 
 static void clock_tree_show_one(struct seq_file *s, struct clk *c, int level)
 {
        struct clk *child;
        struct clk *safe;
+       struct dvfs *d;
        const char *state = "uninit";
        char div[8] = {0};
 
@@ -426,6 +550,10 @@ static void clock_tree_show_one(struct seq_file *s, struct clk *c, int level)
                !c->set ? '*' : ' ',
                30 - level * 3, c->name,
                state, c->refcnt, div, c->rate);
+
+       list_for_each_entry(d, &c->dvfs, node)
+               dvfs_show_one(s, d, level + 1);
+
        list_for_each_entry_safe(child, safe, &c->children, sibling) {
                clock_tree_show_one(s, child, level + 1);
        }
@@ -555,6 +683,9 @@ static int __init clk_debugfs_init(void)
        if (!d)
                goto err_out;
 
+       if (dvfs_debugfs_init(clk_debugfs_root))
+               goto err_out;
+
        list_for_each_entry(c, &clocks, node) {
                err = clk_debugfs_register(c);
                if (err)
index 1e0b0369df5a00a36c8cd5d4118c64aa5a288cc1..ea907886ebb97e2837f02c8a065db34e9e1f4799 100644 (file)
@@ -84,8 +84,12 @@ struct clk {
        struct clk_ops                  *ops;
        struct clk                      *parent;
        struct clk_lookup               lookup;
+       unsigned long                   dvfs_rate;
        unsigned long                   rate;
+       unsigned long                   requested_rate;
        unsigned long                   max_rate;
+       bool                            is_dvfs;
+       bool                            auto_dvfs;
        u32                             flags;
        u32                             refcnt;
        const char                      *name;
@@ -119,6 +123,8 @@ struct clk {
        /* Virtual cpu clock */
        struct clk                      *main;
        struct clk                      *backup;
+
+       struct list_head                dvfs;
 };
 
 
@@ -146,5 +152,6 @@ int clk_set_parent_locked(struct clk *c, struct clk *parent);
 int clk_set_rate_locked(struct clk *c, unsigned long rate);
 int clk_reparent(struct clk *c, struct clk *parent);
 void tegra_clk_init_from_table(struct tegra_clk_init_table *table);
+void tegra_clk_set_dvfs_rates(void);
 
 #endif
diff --git a/arch/arm/mach-tegra/dvfs.c b/arch/arm/mach-tegra/dvfs.c
new file mode 100644 (file)
index 0000000..0a2135e
--- /dev/null
@@ -0,0 +1,320 @@
+/*
+ *
+ * Copyright (C) 2010 Google, Inc.
+ *
+ * Author:
+ *     Colin Cross <ccross@google.com>
+ *
+ * This software is licensed under the terms of the GNU General Public
+ * License version 2, as published by the Free Software Foundation, and
+ * may be copied, distributed, and modified under those terms.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+#include <linux/kernel.h>
+#include <linux/clk.h>
+#include <linux/list.h>
+#include <linux/init.h>
+#include <linux/list_sort.h>
+#include <linux/module.h>
+#include <linux/debugfs.h>
+#include <linux/slab.h>
+#include <linux/seq_file.h>
+#include <linux/regulator/consumer.h>
+#include <asm/clkdev.h>
+#include <mach/clk.h>
+
+#include "board.h"
+#include "clock.h"
+#include "dvfs.h"
+
+struct dvfs_reg {
+       struct list_head node;  /* node in dvfs_reg_list */
+       struct list_head dvfs;  /* list head of attached dvfs clocks */
+       const char *reg_id;
+       struct regulator *reg;
+       int max_millivolts;
+       int millivolts;
+};
+
+static LIST_HEAD(dvfs_list);
+static LIST_HEAD(dvfs_debug_list);
+static LIST_HEAD(dvfs_reg_list);
+
+static DEFINE_MUTEX(dvfs_lock);
+
+void lock_dvfs(void)
+{
+       mutex_lock(&dvfs_lock);
+}
+
+void unlock_dvfs(void)
+{
+       mutex_unlock(&dvfs_lock);
+}
+
+static int dvfs_reg_set_voltage(struct dvfs_reg *dvfs_reg)
+{
+       int millivolts = 0;
+       struct dvfs *d;
+
+       list_for_each_entry(d, &dvfs_reg->dvfs, reg_node)
+               millivolts = max(d->cur_millivolts, millivolts);
+
+       if (millivolts == dvfs_reg->millivolts)
+               return 0;
+
+       dvfs_reg->millivolts = millivolts;
+
+       return regulator_set_voltage(dvfs_reg->reg,
+               millivolts * 1000, dvfs_reg->max_millivolts * 1000);
+}
+
+static int dvfs_reg_get_voltage(struct dvfs_reg *dvfs_reg)
+{
+       int ret = regulator_get_voltage(dvfs_reg->reg);
+
+       if (ret > 0)
+               return ret / 1000;
+
+       return ret;
+}
+
+static struct dvfs_reg *get_dvfs_reg(struct dvfs *d)
+{
+       struct dvfs_reg *dvfs_reg;
+       struct regulator *reg;
+
+       list_for_each_entry(dvfs_reg, &dvfs_reg_list, node)
+               if (!strcmp(d->reg_id, dvfs_reg->reg_id))
+                       return dvfs_reg;
+
+       reg = regulator_get(NULL, d->reg_id);
+       if (IS_ERR(reg))
+               return NULL;
+
+       dvfs_reg = kzalloc(sizeof(struct dvfs_reg), GFP_KERNEL);
+       if (!dvfs_reg) {
+               pr_err("%s: Failed to allocate dvfs_reg\n", __func__);
+               regulator_put(reg);
+               return NULL;
+       }
+
+       INIT_LIST_HEAD(&dvfs_reg->dvfs);
+       dvfs_reg->reg = reg;
+       dvfs_reg->reg_id = kstrdup(d->reg_id, GFP_KERNEL);
+
+       list_add_tail(&dvfs_reg->node, &dvfs_reg_list);
+
+       return dvfs_reg;
+}
+
+static struct dvfs_reg *attach_dvfs_reg(struct dvfs *d)
+{
+       struct dvfs_reg *dvfs_reg;
+
+       dvfs_reg = get_dvfs_reg(d);
+       if (!dvfs_reg)
+               return NULL;
+
+       list_add_tail(&d->reg_node, &dvfs_reg->dvfs);
+       d->dvfs_reg = dvfs_reg;
+       if (d->max_millivolts > d->dvfs_reg->max_millivolts)
+               d->dvfs_reg->max_millivolts = d->max_millivolts;
+
+       d->cur_millivolts = dvfs_reg_get_voltage(d->dvfs_reg);
+
+       return dvfs_reg;
+}
+
+static int
+__tegra_dvfs_set_rate(struct clk *c, struct dvfs *d, unsigned long rate)
+{
+       int i = 0;
+       int ret;
+
+       if (d->freqs == NULL || d->millivolts == NULL)
+               return -ENODEV;
+
+       if (rate > d->freqs[d->num_freqs - 1]) {
+               pr_warn("tegra_dvfs: rate %lu too high for dvfs on %s\n", rate,
+                       c->name);
+               return -EINVAL;
+       }
+
+       if (rate == 0) {
+               d->cur_millivolts = 0;
+       } else {
+               while (i < d->num_freqs && rate > d->freqs[i])
+                       i++;
+
+               d->cur_millivolts = d->millivolts[i];
+       }
+
+       d->cur_rate = rate;
+
+       if (!d->dvfs_reg)
+               return 0;
+
+       ret = dvfs_reg_set_voltage(d->dvfs_reg);
+       if (ret)
+               pr_err("Failed to set regulator %s for clock %s to %d mV\n",
+                       d->dvfs_reg->reg_id, c->name, d->cur_millivolts);
+
+       return ret;
+}
+
+int tegra_dvfs_set_rate(struct clk *c, unsigned long rate)
+{
+       struct dvfs *d;
+       int ret = 0;
+       bool freq_up;
+
+       c->dvfs_rate = rate;
+
+       freq_up = (c->refcnt == 0) || (rate > c->rate);
+
+       list_for_each_entry(d, &c->dvfs, node) {
+               if (d->higher == freq_up)
+                       ret = __tegra_dvfs_set_rate(c, d, rate);
+               if (ret)
+                       return ret;
+       }
+
+       list_for_each_entry(d, &c->dvfs, node) {
+               if (d->higher != freq_up)
+                       ret = __tegra_dvfs_set_rate(c, d, rate);
+               if (ret)
+                       return ret;
+       }
+
+       return 0;
+}
+EXPORT_SYMBOL(tegra_dvfs_set_rate);
+
+int tegra_enable_dvfs_on_clk(struct clk *c, struct dvfs *d)
+{
+       int i;
+       struct dvfs_reg *dvfs_reg;
+
+       dvfs_reg = attach_dvfs_reg(d);
+       if (!dvfs_reg) {
+               pr_err("Failed to get regulator %s for clock %s\n",
+                       d->reg_id, c->name);
+               return -EINVAL;
+       }
+
+       for (i = 0; i < MAX_DVFS_FREQS; i++) {
+               if (d->millivolts[i] == 0)
+                       break;
+
+               d->freqs[i] *= d->freqs_mult;
+
+               /* If final frequencies are 0, pad with previous frequency */
+               if (d->freqs[i] == 0 && i > 1)
+                       d->freqs[i] = d->freqs[i - 1];
+       }
+       d->num_freqs = i;
+
+       if (d->auto_dvfs)
+               c->auto_dvfs = true;
+
+       c->is_dvfs = true;
+       smp_wmb();
+
+       list_add_tail(&d->node, &c->dvfs);
+
+       list_add_tail(&d->debug_node, &dvfs_debug_list);
+
+       return 0;
+}
+
+int __init tegra_init_dvfs(void)
+{
+       lock_dvfs();
+       tegra2_init_dvfs();
+
+       tegra_clk_set_dvfs_rates();
+       unlock_dvfs();
+
+       return 0;
+}
+late_initcall(tegra_init_dvfs);
+
+#ifdef CONFIG_DEBUG_FS
+static int dvfs_tree_sort_cmp(void *p, struct list_head *a, struct list_head *b)
+{
+       struct dvfs *da = list_entry(a, struct dvfs, debug_node);
+       struct dvfs *db = list_entry(b, struct dvfs, debug_node);
+       int ret;
+
+       ret = strcmp(da->reg_id, db->reg_id);
+       if (ret != 0)
+               return ret;
+
+       if (da->cur_millivolts < db->cur_millivolts)
+               return 1;
+       if (da->cur_millivolts > db->cur_millivolts)
+               return -1;
+
+       return strcmp(da->clk_name, db->clk_name);
+}
+
+static int dvfs_tree_show(struct seq_file *s, void *data)
+{
+       struct dvfs *d;
+       const char *last_reg = "";
+
+       seq_printf(s, "   clock      rate       mV\n");
+       seq_printf(s, "--------------------------------\n");
+
+       lock_dvfs();
+
+       list_sort(NULL, &dvfs_debug_list, dvfs_tree_sort_cmp);
+
+       list_for_each_entry(d, &dvfs_debug_list, debug_node) {
+               if (strcmp(last_reg, d->dvfs_reg->reg_id) != 0) {
+                       last_reg = d->dvfs_reg->reg_id;
+                       seq_printf(s, "%s %d mV:\n", d->dvfs_reg->reg_id,
+                               d->dvfs_reg->millivolts);
+               }
+
+               seq_printf(s, "   %-10s %-10lu %-4d mV\n", d->clk_name,
+                       d->cur_rate, d->cur_millivolts);
+       }
+
+       unlock_dvfs();
+
+       return 0;
+}
+
+static int dvfs_tree_open(struct inode *inode, struct file *file)
+{
+       return single_open(file, dvfs_tree_show, inode->i_private);
+}
+
+static const struct file_operations dvfs_tree_fops = {
+       .open           = dvfs_tree_open,
+       .read           = seq_read,
+       .llseek         = seq_lseek,
+       .release        = single_release,
+};
+
+int __init dvfs_debugfs_init(struct dentry *clk_debugfs_root)
+{
+       struct dentry *d;
+
+       d = debugfs_create_file("dvfs", S_IRUGO, clk_debugfs_root, NULL,
+               &dvfs_tree_fops);
+       if (!d)
+               return -ENOMEM;
+
+       return 0;
+}
+
+#endif
diff --git a/arch/arm/mach-tegra/dvfs.h b/arch/arm/mach-tegra/dvfs.h
new file mode 100644 (file)
index 0000000..df6a386
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ *
+ * Copyright (C) 2010 Google, Inc.
+ *
+ * Author:
+ *     Colin Cross <ccross@google.com>
+ *
+ * This software is licensed under the terms of the GNU General Public
+ * License version 2, as published by the Free Software Foundation, and
+ * may be copied, distributed, and modified under those terms.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+#ifndef _TEGRA_DVFS_H_
+#define _TEGRA_DVFS_H_
+
+#define MAX_DVFS_FREQS 16
+
+struct clk;
+
+struct dvfs {
+       /* Used only by tegra2_clock.c */
+       const char *clk_name;
+       int process_id;
+       bool cpu;
+
+       /* Must be initialized before tegra_dvfs_init */
+       const char *reg_id;
+       int freqs_mult;
+       unsigned long freqs[MAX_DVFS_FREQS];
+       unsigned long millivolts[MAX_DVFS_FREQS];
+       bool auto_dvfs;
+       bool higher;
+
+       /* Filled in by tegra_dvfs_init */
+       int max_millivolts;
+       int num_freqs;
+       struct dvfs_reg *dvfs_reg;
+
+       int cur_millivolts;
+       unsigned long cur_rate;
+       struct list_head node;
+       struct list_head debug_node;
+       struct list_head reg_node;
+};
+
+void lock_dvfs(void);
+void unlock_dvfs(void);
+
+void tegra2_init_dvfs(void);
+int tegra_enable_dvfs_on_clk(struct clk *c, struct dvfs *d);
+int dvfs_debugfs_init(struct dentry *clk_debugfs_root);
+
+#endif
index 2896f25ebfb57f0ce29cc43a4965763284166422..04ff7b672ad8a4334d65d33b2ad552d5315d3e5d 100644 (file)
 #ifndef __MACH_CLK_H
 #define __MACH_CLK_H
 
+struct dvfs;
+
 void tegra_periph_reset_deassert(struct clk *c);
 void tegra_periph_reset_assert(struct clk *c);
 
+int tegra_dvfs_set_rate(struct clk *c, unsigned long rate);
+
 #endif