From 8f716a774a4cde56db58597f08c9370f1c66fb3d Mon Sep 17 00:00:00 2001 From: Colin Cross Date: Wed, 8 Sep 2010 19:46:13 -0700 Subject: [PATCH] [ARM] tegra: clock: Add new dvfs 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 --- arch/arm/mach-tegra/Makefile | 1 + arch/arm/mach-tegra/clock.c | 149 +++++++++++- arch/arm/mach-tegra/clock.h | 7 + arch/arm/mach-tegra/dvfs.c | 320 +++++++++++++++++++++++++ arch/arm/mach-tegra/dvfs.h | 59 +++++ arch/arm/mach-tegra/include/mach/clk.h | 4 + 6 files changed, 531 insertions(+), 9 deletions(-) create mode 100644 arch/arm/mach-tegra/dvfs.c create mode 100644 arch/arm/mach-tegra/dvfs.h diff --git a/arch/arm/mach-tegra/Makefile b/arch/arm/mach-tegra/Makefile index 22e59f357a92..7587d245bfeb 100644 --- a/arch/arm/mach-tegra/Makefile +++ b/arch/arm/mach-tegra/Makefile @@ -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 diff --git a/arch/arm/mach-tegra/clock.c b/arch/arm/mach-tegra/clock.c index cca4a66faa01..09b5993c5513 100644 --- a/arch/arm/mach-tegra/clock.c +++ b/arch/arm/mach-tegra/clock.c @@ -25,13 +25,40 @@ #include #include #include +#include #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) diff --git a/arch/arm/mach-tegra/clock.h b/arch/arm/mach-tegra/clock.h index 1e0b0369df5a..ea907886ebb9 100644 --- a/arch/arm/mach-tegra/clock.h +++ b/arch/arm/mach-tegra/clock.h @@ -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 index 000000000000..0a2135e3b784 --- /dev/null +++ b/arch/arm/mach-tegra/dvfs.c @@ -0,0 +1,320 @@ +/* + * + * Copyright (C) 2010 Google, Inc. + * + * Author: + * Colin Cross + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 index 000000000000..df6a3866d31b --- /dev/null +++ b/arch/arm/mach-tegra/dvfs.h @@ -0,0 +1,59 @@ +/* + * + * Copyright (C) 2010 Google, Inc. + * + * Author: + * Colin Cross + * + * 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 diff --git a/arch/arm/mach-tegra/include/mach/clk.h b/arch/arm/mach-tegra/include/mach/clk.h index 2896f25ebfb5..04ff7b672ad8 100644 --- a/arch/arm/mach-tegra/include/mach/clk.h +++ b/arch/arm/mach-tegra/include/mach/clk.h @@ -20,7 +20,11 @@ #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 -- 2.34.1