/* linux/arch/arm/plat-s3c24xx/cpu-freq.c * * Copyright (c) 2006-2008 Simtec Electronics * http://armlinux.simtec.co.uk/ * Ben Dooks * * S3C24XX CPU Frequency scaling * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* note, cpufreq support deals in kHz, no Hz */ static struct cpufreq_driver s3c24xx_driver; static struct s3c_cpufreq_config cpu_cur; static struct s3c_iotimings s3c24xx_iotiming; static struct cpufreq_frequency_table *pll_reg; static unsigned int last_target = ~0; static unsigned int ftab_size; static struct cpufreq_frequency_table *ftab; static struct clk *_clk_mpll; static struct clk *_clk_xtal; static struct clk *clk_fclk; static struct clk *clk_hclk; static struct clk *clk_pclk; static struct clk *clk_arm; #ifdef CONFIG_CPU_FREQ_S3C24XX_DEBUGFS struct s3c_cpufreq_config *s3c_cpufreq_getconfig(void) { return &cpu_cur; } struct s3c_iotimings *s3c_cpufreq_getiotimings(void) { return &s3c24xx_iotiming; } #endif /* CONFIG_CPU_FREQ_S3C24XX_DEBUGFS */ static void s3c_cpufreq_getcur(struct s3c_cpufreq_config *cfg) { unsigned long fclk, pclk, hclk, armclk; cfg->freq.fclk = fclk = clk_get_rate(clk_fclk); cfg->freq.hclk = hclk = clk_get_rate(clk_hclk); cfg->freq.pclk = pclk = clk_get_rate(clk_pclk); cfg->freq.armclk = armclk = clk_get_rate(clk_arm); cfg->pll.index = __raw_readl(S3C2410_MPLLCON); cfg->pll.frequency = fclk; cfg->freq.hclk_tns = 1000000000 / (cfg->freq.hclk / 10); cfg->divs.h_divisor = fclk / hclk; cfg->divs.p_divisor = fclk / pclk; } static inline void s3c_cpufreq_calc(struct s3c_cpufreq_config *cfg) { unsigned long pll = cfg->pll.frequency; cfg->freq.fclk = pll; cfg->freq.hclk = pll / cfg->divs.h_divisor; cfg->freq.pclk = pll / cfg->divs.p_divisor; /* convert hclk into 10ths of nanoseconds for io calcs */ cfg->freq.hclk_tns = 1000000000 / (cfg->freq.hclk / 10); } static inline int closer(unsigned int target, unsigned int n, unsigned int c) { int diff_cur = abs(target - c); int diff_new = abs(target - n); return (diff_new < diff_cur); } static void s3c_cpufreq_show(const char *pfx, struct s3c_cpufreq_config *cfg) { s3c_freq_dbg("%s: Fvco=%u, F=%lu, A=%lu, H=%lu (%u), P=%lu (%u)\n", pfx, cfg->pll.frequency, cfg->freq.fclk, cfg->freq.armclk, cfg->freq.hclk, cfg->divs.h_divisor, cfg->freq.pclk, cfg->divs.p_divisor); } /* functions to wrapper the driver info calls to do the cpu specific work */ static void s3c_cpufreq_setio(struct s3c_cpufreq_config *cfg) { if (cfg->info->set_iotiming) (cfg->info->set_iotiming)(cfg, &s3c24xx_iotiming); } static int s3c_cpufreq_calcio(struct s3c_cpufreq_config *cfg) { if (cfg->info->calc_iotiming) return (cfg->info->calc_iotiming)(cfg, &s3c24xx_iotiming); return 0; } static void s3c_cpufreq_setrefresh(struct s3c_cpufreq_config *cfg) { (cfg->info->set_refresh)(cfg); } static void s3c_cpufreq_setdivs(struct s3c_cpufreq_config *cfg) { (cfg->info->set_divs)(cfg); } static int s3c_cpufreq_calcdivs(struct s3c_cpufreq_config *cfg) { return (cfg->info->calc_divs)(cfg); } static void s3c_cpufreq_setfvco(struct s3c_cpufreq_config *cfg) { (cfg->info->set_fvco)(cfg); } static inline void s3c_cpufreq_resume_clocks(void) { cpu_cur.info->resume_clocks(); } static inline void s3c_cpufreq_updateclk(struct clk *clk, unsigned int freq) { clk_set_rate(clk, freq); } static int s3c_cpufreq_settarget(struct cpufreq_policy *policy, unsigned int target_freq, struct cpufreq_frequency_table *pll) { struct s3c_cpufreq_freqs freqs; struct s3c_cpufreq_config cpu_new; unsigned long flags; cpu_new = cpu_cur; /* copy new from current */ s3c_cpufreq_show("cur", &cpu_cur); /* TODO - check for DMA currently outstanding */ cpu_new.pll = pll ? *pll : cpu_cur.pll; if (pll) freqs.pll_changing = 1; /* update our frequencies */ cpu_new.freq.armclk = target_freq; cpu_new.freq.fclk = cpu_new.pll.frequency; if (s3c_cpufreq_calcdivs(&cpu_new) < 0) { printk(KERN_ERR "no divisors for %d\n", target_freq); goto err_notpossible; } s3c_freq_dbg("%s: got divs\n", __func__); s3c_cpufreq_calc(&cpu_new); s3c_freq_dbg("%s: calculated frequencies for new\n", __func__); if (cpu_new.freq.hclk != cpu_cur.freq.hclk) { if (s3c_cpufreq_calcio(&cpu_new) < 0) { printk(KERN_ERR "%s: no IO timings\n", __func__); goto err_notpossible; } } s3c_cpufreq_show("new", &cpu_new); /* setup our cpufreq parameters */ freqs.old = cpu_cur.freq; freqs.new = cpu_new.freq; freqs.freqs.cpu = 0; freqs.freqs.old = cpu_cur.freq.armclk / 1000; freqs.freqs.new = cpu_new.freq.armclk / 1000; /* update f/h/p clock settings before we issue the change * notification, so that drivers do not need to do anything * special if they want to recalculate on CPUFREQ_PRECHANGE. */ s3c_cpufreq_updateclk(_clk_mpll, cpu_new.pll.frequency); s3c_cpufreq_updateclk(clk_fclk, cpu_new.freq.fclk); s3c_cpufreq_updateclk(clk_hclk, cpu_new.freq.hclk); s3c_cpufreq_updateclk(clk_pclk, cpu_new.freq.pclk); /* start the frequency change */ if (policy) cpufreq_notify_transition(&freqs.freqs, CPUFREQ_PRECHANGE); /* If hclk is staying the same, then we do not need to * re-write the IO or the refresh timings whilst we are changing * speed. */ local_irq_save(flags); /* is our memory clock slowing down? */ if (cpu_new.freq.hclk < cpu_cur.freq.hclk) { s3c_cpufreq_setrefresh(&cpu_new); s3c_cpufreq_setio(&cpu_new); } if (cpu_new.freq.fclk == cpu_cur.freq.fclk) { /* not changing PLL, just set the divisors */ s3c_cpufreq_setdivs(&cpu_new); } else { if (cpu_new.freq.fclk < cpu_cur.freq.fclk) { /* slow the cpu down, then set divisors */ s3c_cpufreq_setfvco(&cpu_new); s3c_cpufreq_setdivs(&cpu_new); } else { /* set the divisors, then speed up */ s3c_cpufreq_setdivs(&cpu_new); s3c_cpufreq_setfvco(&cpu_new); } } /* did our memory clock speed up */ if (cpu_new.freq.hclk > cpu_cur.freq.hclk) { s3c_cpufreq_setrefresh(&cpu_new); s3c_cpufreq_setio(&cpu_new); } /* update our current settings */ cpu_cur = cpu_new; local_irq_restore(flags); /* notify everyone we've done this */ if (policy) cpufreq_notify_transition(&freqs.freqs, CPUFREQ_POSTCHANGE); s3c_freq_dbg("%s: finished\n", __func__); return 0; err_notpossible: printk(KERN_ERR "no compatible settings for %d\n", target_freq); return -EINVAL; } /* s3c_cpufreq_target * * called by the cpufreq core to adjust the frequency that the CPU * is currently running at. */ static int s3c_cpufreq_target(struct cpufreq_policy *policy, unsigned int target_freq, unsigned int relation) { struct cpufreq_frequency_table *pll; unsigned int index; /* avoid repeated calls which cause a needless amout of duplicated * logging output (and CPU time as the calculation process is * done) */ if (target_freq == last_target) return 0; last_target = target_freq; s3c_freq_dbg("%s: policy %p, target %u, relation %u\n", __func__, policy, target_freq, relation); if (ftab) { if (cpufreq_frequency_table_target(policy, ftab, target_freq, relation, &index)) { s3c_freq_dbg("%s: table failed\n", __func__); return -EINVAL; } s3c_freq_dbg("%s: adjust %d to entry %d (%u)\n", __func__, target_freq, index, ftab[index].frequency); target_freq = ftab[index].frequency; } target_freq *= 1000; /* convert target to Hz */ /* find the settings for our new frequency */ if (!pll_reg || cpu_cur.lock_pll) { /* either we've not got any PLL values, or we've locked * to the current one. */ pll = NULL; } else { struct cpufreq_policy tmp_policy; int ret; /* we keep the cpu pll table in Hz, to ensure we get an * accurate value for the PLL output. */