diff options
Diffstat (limited to 'arch/arm/mach-mx35/dvfs.c')
-rw-r--r-- | arch/arm/mach-mx35/dvfs.c | 598 |
1 files changed, 598 insertions, 0 deletions
diff --git a/arch/arm/mach-mx35/dvfs.c b/arch/arm/mach-mx35/dvfs.c new file mode 100644 index 000000000000..05477a9bc9f5 --- /dev/null +++ b/arch/arm/mach-mx35/dvfs.c @@ -0,0 +1,598 @@ +/* + * Copyright 2008-2009 Freescale Semiconductor, Inc. All Rights Reserved. + */ + +/* + * The code contained herein is licensed under the GNU General Public + * License. You may obtain a copy of the GNU General Public License + * Version 2 or later at the following locations: + * + * http://www.opensource.org/licenses/gpl-license.html + * http://www.gnu.org/copyleft/gpl.html + */ + +/* + * @file dvfs.c + * + * @brief A simplied driver for the Freescale Semiconductor MXC DVFS module. + * + * Upon initialization, the DVFS driver initializes the DVFS hardware + * sets up driver nodes attaches to the DVFS interrupt and initializes internal + * data structures. When the DVFS interrupt occurs the driver checks the cause + * of the interrupt (lower frequency, increase frequency or emergency) and + * changes the CPU voltage according to translation table that is loaded into + * the driver. + * + * @ingroup PM + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/fs.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/device.h> +#include <linux/sysdev.h> +#include <linux/delay.h> +#include <linux/clk.h> +#include <linux/regulator/regulator.h> +#include <mach/hardware.h> +#include "crm_regs.h" + +/* + * The frequency of div_3_clk will affect the dvfs sample rate.. + */ +#define DVFS_DIV3CK (3 << MXC_CCM_LTR0_DIV3CK_OFFSET) + +/* + * Panic threshold. Panic frequency change request + * will be sent if DVFS counter value will be more than this value. + */ +#define DVFS_PNCTHR (63 << MXC_CCM_LTR1_PNCTHR_OFFSET) + +/* + * Load tracking buffer source: 1 for ld_add; 0 for pre_ld_add + */ +#define DVFS_LTBRSR (1 << MXC_CCM_LTR1_LTBRSR_OFFSET) + +/* EMAC defines how many samples are included in EMA calculation */ +#define DVFS_EMAC (0x20 << MXC_CCM_LTR2_EMAC_OFFSET) + +/* + * Frequency increase threshold. Increase frequency change request + * will be sent if DVFS counter value will be more than this value. + */ +#define DVFS_UPTHR(val) (val << MXC_CCM_LTR0_UPTHR_OFFSET) + +/* + * Frequency decrease threshold. Decrease frequency change request + * will be sent if DVFS counter value will be less than this value. + */ +#define DVFS_DNTHR(val) (val << MXC_CCM_LTR0_DNTHR_OFFSET) + +/* + * DNCNT defines the amount of times the down threshold should be exceeded + * before DVFS will trigger frequency decrease request. + */ +#define DVFS_DNCNT(val) (val << MXC_CCM_LTR1_DNCNT_OFFSET) + +/* + * UPCNT defines the amount of times the up threshold should be exceeded + * before DVFS will trigger frequency increase request. + */ +#define DVFS_UPCNT(val) (val << MXC_CCM_LTR1_UPCNT_OFFSET) + +#define DVFS_DVSUP(val) (val << MXC_CCM_PMCR0_DVSUP_OFFSET) + +#define MXC_DVFS_MAX_WP_NUM 2 + +enum { + FSVAI_FREQ_NOCHANGE = 0x0, + FSVAI_FREQ_INCREASE, + FSVAI_FREQ_DECREASE, + FSVAI_FREQ_EMERG, +}; + +struct dvfs_wp { + unsigned long cpu_rate; + u32 core_voltage; + u32 dvsup; + u32 dnthr; + u32 upthr; + u32 dncnt; + u32 upcnt; +}; + +/* the default working points for MX35 TO2 DVFS. */ +static struct dvfs_wp dvfs_wp_tbl[MXC_DVFS_MAX_WP_NUM] = { + {399000000, 1200000, DVFS_DVSUP(DVSUP_LOW), DVFS_DNTHR(18), + DVFS_UPTHR(31), DVFS_DNCNT(0x33), + DVFS_UPCNT(0x33)}, +/* TBD: Need to set default voltage according to published data sheet */ + {532000000, 1350000, DVFS_DVSUP(DVSUP_TURBO), DVFS_DNTHR(18), + DVFS_UPTHR(30), DVFS_DNCNT(0x33), + DVFS_UPCNT(0x33)} +}; + +static u8 dvfs_wp_num = MXC_DVFS_MAX_WP_NUM; + + /* Used for tracking the number of interrupts */ +static u32 dvfs_nr_up[MXC_DVFS_MAX_WP_NUM]; +static u32 dvfs_nr_dn[MXC_DVFS_MAX_WP_NUM]; +static unsigned long stored_cpu_rate; /* cpu rate before DVFS starts */ +static u32 stored_pmcr0; +static int dvfs_is_active; /* indicate DVFS is active or not */ + +static struct delayed_work dvfs_work; + +/* + * Clock structures + */ +static struct clk *cpu_clk; +static struct regulator *core_reg; + +const static u8 ltr_gp_weight[] = { + 0, /* 0 */ + 0, + 0, + 0, + 0, + 0, /* 5 */ + 0, + 0, + 0, + 0, + 0, /* 10 */ + 0, + 0, + 0, + 0, + 0, /* 15 */ +}; + +DEFINE_SPINLOCK(mxc_dvfs_lock); + +/*! + * This function sets the weight of general purpose signals + * @param gp_id number of general purpose bit + * @param weight the weight of the general purpose bit + */ +static void set_gp_weight(int gp_id, u8 weight) +{ + u32 reg; + + if (gp_id < 9) { + reg = __raw_readl(MXC_CCM_LTR3); + reg = (reg & ~(MXC_CCM_LTR3_WSW_MASK(gp_id))) | + (weight << MXC_CCM_LTR3_WSW_OFFSET(gp_id)); + __raw_writel(reg, MXC_CCM_LTR3); + } else if (gp_id < 16) { + reg = __raw_readl(MXC_CCM_LTR2); + reg = (reg & ~(MXC_CCM_LTR2_WSW_MASK(gp_id))) | + (weight << MXC_CCM_LTR2_WSW_OFFSET(gp_id)); + __raw_writel(reg, MXC_CCM_LTR2); + } +} + +/*! + * This function sets upper threshold, lower threshold, + * up-counter, down-counter for load tracking. + * @param upthr upper threshold + * @param dnthr lower threshold + * @param upcnt up counter + * @param dncnt down counter + */ +static void set_ltr_thres_counter(u32 upthr, u32 dnthr, u32 upcnt, u32 dncnt) +{ + u32 reg; + reg = __raw_readl(MXC_CCM_LTR0); + reg = + (reg & + ~(MXC_CCM_LTR0_UPTHR_MASK | + MXC_CCM_LTR0_DNTHR_MASK)) | upthr | dnthr; + __raw_writel(reg, MXC_CCM_LTR0); + + reg = __raw_readl(MXC_CCM_LTR1); + reg = + (reg & + ~(MXC_CCM_LTR1_UPCNT_MASK | + MXC_CCM_LTR1_DNCNT_MASK)) | upcnt | dncnt; + __raw_writel(reg, MXC_CCM_LTR1); +} + +/*! + * This function is called for module initialization. + * It sets up the DVFS hardware. + * It sets default values for DVFS thresholds and counters. The default + * values was chosen from a set of different reasonable values. They was tested + * and the default values in the driver gave the best results. + * More work should be done to find optimal values. + * + * @return 0 if successful; non-zero otherwise. + * + */ +static int init_dvfs_controller(void) +{ + u32 i, reg; + + /* setup LTR0 */ + reg = __raw_readl(MXC_CCM_LTR0); + reg = (reg & ~(MXC_CCM_LTR0_DIV3CK_MASK)) | DVFS_DIV3CK; + __raw_writel(reg, MXC_CCM_LTR0); + + /* set up LTR1 */ + reg = __raw_readl(MXC_CCM_LTR1); + reg = (reg & ~(MXC_CCM_LTR1_PNCTHR_MASK | MXC_CCM_LTR1_LTBRSR_MASK)); + reg = reg | DVFS_PNCTHR | DVFS_LTBRSR; + __raw_writel(reg, MXC_CCM_LTR1); + + /* setup LTR2 */ + reg = __raw_readl(MXC_CCM_LTR2); + reg = (reg & ~(MXC_CCM_LTR2_EMAC_MASK)) | DVFS_EMAC; + __raw_writel(reg, MXC_CCM_LTR2); + + /* Set general purpose weights to 0 */ + for (i = 0; i < 16; i++) + set_gp_weight(i, ltr_gp_weight[i]); + + /* ARM interrupt, mask load buf full interrupt */ + reg = __raw_readl(MXC_CCM_PMCR0); + reg |= MXC_CCM_PMCR0_DVFIS | MXC_CCM_PMCR0_LBMI; + __raw_writel(reg, MXC_CCM_PMCR0); + + return 0; +} + +static void dvfs_workqueue_handler(struct work_struct *work) +{ + u32 pmcr0 = stored_pmcr0; + u32 fsvai = (pmcr0 & MXC_CCM_PMCR0_FSVAI_MASK) >> + MXC_CCM_PMCR0_FSVAI_OFFSET; + u32 dvsup = (pmcr0 & MXC_CCM_PMCR0_DVSUP_MASK) >> + MXC_CCM_PMCR0_DVSUP_OFFSET; + u32 curr_cpu; + u8 curr_dvfs; + + if (!dvfs_is_active) + return; + + if (fsvai == FSVAI_FREQ_NOCHANGE) { + /* Do nothing. Freq change is not required */ + printk(KERN_WARNING "fsvai should not be 0\n"); + goto exit; + } + + if (((dvsup == DVSUP_LOW) && (fsvai == FSVAI_FREQ_DECREASE)) || + ((dvsup == DVSUP_TURBO) && ((fsvai == FSVAI_FREQ_INCREASE) || + (fsvai == FSVAI_FREQ_EMERG)))) { + /* Interrupt should be disabled in these cases according to + * the spec since DVFS is already at lowest (highest) state */ + printk(KERN_WARNING "Something is wrong?\n"); + goto exit; + } + + /*Disable DPTC voltage update */ + pmcr0 = pmcr0 & ~MXC_CCM_PMCR0_DPVCR; + __raw_writel(pmcr0, MXC_CCM_PMCR0); + + curr_cpu = clk_get_rate(cpu_clk); + for (curr_dvfs = 0; curr_dvfs < dvfs_wp_num; curr_dvfs++) { + if (dvfs_wp_tbl[curr_dvfs].cpu_rate == curr_cpu) { + if (fsvai == FSVAI_FREQ_DECREASE) { + curr_dvfs--; + dvfs_nr_dn[dvsup]++; + /*reduce frequency and then voltage */ + clk_set_rate(cpu_clk, + dvfs_wp_tbl[curr_dvfs].cpu_rate); + regulator_set_voltage(core_reg, + dvfs_wp_tbl[curr_dvfs]. + core_voltage); + pr_info("Decrease frequency to: %ld \n", + dvfs_wp_tbl[curr_dvfs].cpu_rate); + } else { + /*increase freq to the highest one */ + curr_dvfs = dvfs_wp_num - 1; + dvfs_nr_up[dvsup]++; + /*Increase voltage and then frequency */ + regulator_set_voltage(core_reg, + dvfs_wp_tbl[curr_dvfs]. + core_voltage); + clk_set_rate(cpu_clk, + dvfs_wp_tbl[curr_dvfs].cpu_rate); + pr_info("Increase frequency to: %ld \n", + dvfs_wp_tbl[curr_dvfs].cpu_rate); + } + pmcr0 = (pmcr0 & ~MXC_CCM_PMCR0_DVSUP_MASK) + | (dvfs_wp_tbl[curr_dvfs].dvsup); + __raw_writel(pmcr0, MXC_CCM_PMCR0); + + set_ltr_thres_counter(dvfs_wp_tbl[curr_dvfs].upthr, + dvfs_wp_tbl[curr_dvfs].dnthr, + dvfs_wp_tbl[curr_dvfs].upcnt, + dvfs_wp_tbl[curr_dvfs].dncnt); + break; + } + } + + exit: + /* unmask interrupt */ + pmcr0 = pmcr0 & ~MXC_CCM_PMCR0_FSVAIM; + __raw_writel(pmcr0, MXC_CCM_PMCR0); + /*DVFS update finish */ + pmcr0 = (pmcr0 | MXC_CCM_PMCR0_DVFS_UPDATE_FINISH); + __raw_writel(pmcr0, MXC_CCM_PMCR0); +} + +static irqreturn_t dvfs_irq(int irq, void *dev_id) +{ + + u32 pmcr0 = __raw_readl(MXC_CCM_PMCR0); + + /* Config dvfs_start bit */ + pmcr0 = pmcr0 | MXC_CCM_PMCR0_DVFS_START; + /*Mask interrupt */ + pmcr0 = pmcr0 | MXC_CCM_PMCR0_FSVAIM; + __raw_writel(pmcr0, MXC_CCM_PMCR0); + + stored_pmcr0 = pmcr0; + schedule_delayed_work(&dvfs_work, 0); + + return IRQ_RETVAL(1); +} + +/*! + * This function enables the DVFS module. + */ +static int start_dvfs(void) +{ + u32 reg = 0; + unsigned long flags; + u8 i; + + if (dvfs_is_active) { + pr_info("DVFS is already started\n"); + return 0; + } + + spin_lock_irqsave(&mxc_dvfs_lock, flags); + + stored_cpu_rate = clk_get_rate(cpu_clk); + for (i = 0; i < dvfs_wp_num; i++) { + if (dvfs_wp_tbl[i].cpu_rate == stored_cpu_rate) { + /*Set LTR0 and LTR1 */ + set_ltr_thres_counter(dvfs_wp_tbl[i].upthr, + dvfs_wp_tbl[i].dnthr, + dvfs_wp_tbl[i].upcnt, + dvfs_wp_tbl[i].dncnt); + + reg = __raw_readl(MXC_CCM_PMCR0); + reg = + (reg & ~MXC_CCM_PMCR0_DVSUP_MASK) | (dvfs_wp_tbl[i]. + dvsup); + /* enable dvfs and interrupt */ + reg = + (reg & ~MXC_CCM_PMCR0_FSVAIM) | MXC_CCM_PMCR0_DVFEN; + + __raw_writel(reg, MXC_CCM_PMCR0); + + dvfs_is_active = 1; + pr_info("DVFS Starts\n"); + break; + } + } + + spin_unlock_irqrestore(&mxc_dvfs_lock, flags); + if (dvfs_is_active) + return 0; + else + return 1; +} + +/*! + * This function disables the DVFS module. + */ +static void stop_dvfs(void) +{ + u32 pmcr0; + unsigned long curr_cpu = clk_get_rate(cpu_clk); + u8 index; + + if (dvfs_is_active) { + + pmcr0 = __raw_readl(MXC_CCM_PMCR0); + /* disable dvfs and its interrupt */ + pmcr0 = (pmcr0 & ~MXC_CCM_PMCR0_DVFEN) | MXC_CCM_PMCR0_FSVAIM; + __raw_writel(pmcr0, MXC_CCM_PMCR0); + + if (stored_cpu_rate < curr_cpu) { + for (index = 0; index < dvfs_wp_num; index++) { + if (dvfs_wp_tbl[index].cpu_rate == + stored_cpu_rate) + break; + } + clk_set_rate(cpu_clk, stored_cpu_rate); + regulator_set_voltage(core_reg, + dvfs_wp_tbl[index].core_voltage); + } else if (stored_cpu_rate > curr_cpu) { + for (index = 0; index < dvfs_wp_num; index++) { + if (dvfs_wp_tbl[index].cpu_rate == + stored_cpu_rate) + break; + } + regulator_set_voltage(core_reg, + dvfs_wp_tbl[index].core_voltage); + clk_set_rate(cpu_clk, stored_cpu_rate); + } + + dvfs_is_active = 0; + } + + pr_info("DVFS is stopped\n"); +} + +static ssize_t dvfs_enable_store(struct sys_device *dev, struct sysdev_attribute *attr, + const char *buf, size_t size) +{ + if (strstr(buf, "1") != NULL) { + if (start_dvfs() != 0) + printk(KERN_ERR "Failed to start DVFS\n"); + } else if (strstr(buf, "0") != NULL) { + stop_dvfs(); + } + + return size; +} + +static ssize_t dvfs_status_show(struct sys_device *dev, struct sysdev_attribute *attr, + char *buf) +{ + int size = 0, i; + + if (dvfs_is_active) + size = sprintf(buf, "DVFS is enabled\n"); + else + size = sprintf(buf, "DVFS is disabled\n"); + + size += sprintf((buf + size), "UP:\t"); + for (i = 0; i < MXC_DVFS_MAX_WP_NUM; i++) + size += sprintf((buf + size), "%d\t", dvfs_nr_up[i]); + size += sprintf((buf + size), "\n"); + + size += sprintf((buf + size), "DOWN:\t"); + for (i = 0; i < MXC_DVFS_MAX_WP_NUM; i++) + size += sprintf((buf + size), "%d\t", dvfs_nr_dn[i]); + size += sprintf((buf + size), "\n"); + + return size; +} + +static ssize_t dvfs_status_store(struct sys_device *dev, struct sysdev_attribute *attr, + const char *buf, size_t size) +{ + if (strstr(buf, "reset") != NULL) { + int i; + for (i = 0; i < MXC_DVFS_MAX_WP_NUM; i++) { + dvfs_nr_up[i] = 0; + dvfs_nr_dn[i] = 0; + } + } + + return size; +} + +static SYSDEV_ATTR(enable, 0200, NULL, dvfs_enable_store); +static SYSDEV_ATTR(status, 0644, dvfs_status_show, dvfs_status_store); + +static struct sysdev_class dvfs_sysclass = { + .name = "dvfs", +}; + +static struct sys_device dvfs_device = { + .id = 0, + .cls = &dvfs_sysclass, +}; + +static int dvfs_sysdev_ctrl_init(void) +{ + int err; + + err = sysdev_class_register(&dvfs_sysclass); + if (!err) + err = sysdev_register(&dvfs_device); + if (!err) { + err = sysdev_create_file(&dvfs_device, &attr_enable); + err = sysdev_create_file(&dvfs_device, &attr_status); + } + + return err; +} + +static void dvfs_sysdev_ctrl_exit(void) +{ + sysdev_remove_file(&dvfs_device, &attr_enable); + sysdev_remove_file(&dvfs_device, &attr_status); + sysdev_unregister(&dvfs_device); + sysdev_class_unregister(&dvfs_sysclass); +} + +static int __init dvfs_init(void) +{ + int err = 0; + u8 index; + unsigned long curr_cpu; + + if (cpu_is_mx35_rev(CHIP_REV_1_0) == 1) { + /* + * Don't support DVFS for auto path in TO1 because + * the voltages under 399M are all 1.2v + */ + if (!(__raw_readl(MXC_CCM_PDR0) & MXC_CCM_PDR0_AUTO_CON)) { + pr_info("MX35 TO1 auto path, no need to use DVFS \n"); + return -1; + } + } + + cpu_clk = clk_get(NULL, "cpu_clk"); + curr_cpu = clk_get_rate(cpu_clk); + + if (board_is_mx35(BOARD_REV_2)) + core_reg = regulator_get(NULL, "SW2"); + else + core_reg = regulator_get(NULL, "SW3"); + + dvfs_is_active = 0; + + /*Set voltage */ + for (index = 0; index < dvfs_wp_num; index++) { + if (dvfs_wp_tbl[index].cpu_rate == curr_cpu) { + regulator_set_voltage(core_reg, + dvfs_wp_tbl[index].core_voltage); + break; + } + } + + err = init_dvfs_controller(); + if (err) { + printk(KERN_ERR "DVFS: Unable to initialize DVFS"); + return err; + } + + INIT_DELAYED_WORK(&dvfs_work, dvfs_workqueue_handler); + + /* request the DVFS interrupt */ + err = request_irq(MXC_INT_DVFS, dvfs_irq, IRQF_DISABLED, "dvfs", NULL); + if (err) { + printk(KERN_ERR "DVFS: Unable to attach to DVFS interrupt"); + return err; + } + + err = dvfs_sysdev_ctrl_init(); + if (err) { + printk(KERN_ERR + "DVFS: Unable to register sysdev entry for dvfs"); + return err; + } + + return err; +} + +static void __exit dvfs_cleanup(void) +{ + stop_dvfs(); + + /* release the DVFS interrupt */ + free_irq(MXC_INT_DVFS, NULL); + + dvfs_sysdev_ctrl_exit(); + + clk_put(cpu_clk); + regulator_put(core_reg, NULL); +} + +module_init(dvfs_init); +module_exit(dvfs_cleanup); + +MODULE_AUTHOR("Freescale Seminconductor, Inc."); +MODULE_DESCRIPTION("DVFS driver"); +MODULE_LICENSE("GPL"); |