/* * 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 #include #include #include #include #include #include #include #include #include #include #include #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, 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, 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, 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, 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_rev(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 && !IS_ERR(core_reg)) { regulator_set_voltage(core_reg, dvfs_wp_tbl[index].core_voltage, 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); } module_init(dvfs_init); module_exit(dvfs_cleanup); MODULE_AUTHOR("Freescale Seminconductor, Inc."); MODULE_DESCRIPTION("DVFS driver"); MODULE_LICENSE("GPL");