| /* | |
| * Copyright (c) 2010-2011 Samsung Electronics Co., Ltd. | |
| * http://www.samsung.com | |
| * | |
| * EXYNOS - CPU frequency scaling support for EXYNOS series | |
| * | |
| * 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 <linux/kernel.h> | |
| #include <linux/err.h> | |
| #include <linux/clk.h> | |
| #include <linux/io.h> | |
| #include <linux/slab.h> | |
| #include <linux/regulator/consumer.h> | |
| #include <linux/cpufreq.h> | |
| #include <linux/suspend.h> | |
| #include <plat/cpu.h> | |
| #include "exynos-cpufreq.h" | |
| static struct exynos_dvfs_info *exynos_info; | |
| static struct regulator *arm_regulator; | |
| static struct cpufreq_freqs freqs; | |
| static unsigned int locking_frequency; | |
| static bool frequency_locked; | |
| static DEFINE_MUTEX(cpufreq_lock); | |
| static int exynos_verify_speed(struct cpufreq_policy *policy) | |
| { | |
| return cpufreq_frequency_table_verify(policy, | |
| exynos_info->freq_table); | |
| } | |
| static unsigned int exynos_getspeed(unsigned int cpu) | |
| { | |
| return clk_get_rate(exynos_info->cpu_clk) / 1000; | |
| } | |
| static int exynos_cpufreq_get_index(unsigned int freq) | |
| { | |
| struct cpufreq_frequency_table *freq_table = exynos_info->freq_table; | |
| int index; | |
| for (index = 0; | |
| freq_table[index].frequency != CPUFREQ_TABLE_END; index++) | |
| if (freq_table[index].frequency == freq) | |
| break; | |
| if (freq_table[index].frequency == CPUFREQ_TABLE_END) | |
| return -EINVAL; | |
| return index; | |
| } | |
| static int exynos_cpufreq_scale(unsigned int target_freq) | |
| { | |
| struct cpufreq_frequency_table *freq_table = exynos_info->freq_table; | |
| unsigned int *volt_table = exynos_info->volt_table; | |
| struct cpufreq_policy *policy = cpufreq_cpu_get(0); | |
| unsigned int arm_volt, safe_arm_volt = 0; | |
| unsigned int mpll_freq_khz = exynos_info->mpll_freq_khz; | |
| int index, old_index; | |
| int ret = 0; | |
| freqs.old = policy->cur; | |
| freqs.new = target_freq; | |
| if (freqs.new == freqs.old) | |
| goto out; | |
| /* | |
| * The policy max have been changed so that we cannot get proper | |
| * old_index with cpufreq_frequency_table_target(). Thus, ignore | |
| * policy and get the index from the raw freqeuncy table. | |
| */ | |
| old_index = exynos_cpufreq_get_index(freqs.old); | |
| if (old_index < 0) { | |
| ret = old_index; | |
| goto out; | |
| } | |
| index = exynos_cpufreq_get_index(target_freq); | |
| if (index < 0) { | |
| ret = index; | |
| goto out; | |
| } | |
| /* | |
| * ARM clock source will be changed APLL to MPLL temporary | |
| * To support this level, need to control regulator for | |
| * required voltage level | |
| */ | |
| if (exynos_info->need_apll_change != NULL) { | |
| if (exynos_info->need_apll_change(old_index, index) && | |
| (freq_table[index].frequency < mpll_freq_khz) && | |
| (freq_table[old_index].frequency < mpll_freq_khz)) | |
| safe_arm_volt = volt_table[exynos_info->pll_safe_idx]; | |
| } | |
| arm_volt = volt_table[index]; | |
| cpufreq_notify_transition(policy, &freqs, CPUFREQ_PRECHANGE); | |
| /* When the new frequency is higher than current frequency */ | |
| if ((freqs.new > freqs.old) && !safe_arm_volt) { | |
| /* Firstly, voltage up to increase frequency */ | |
| ret = regulator_set_voltage(arm_regulator, arm_volt, arm_volt); | |
| if (ret) { | |
| pr_err("%s: failed to set cpu voltage to %d\n", | |
| __func__, arm_volt); | |
| freqs.new = freqs.old; | |
| goto post_notify; | |
| } | |
| } | |
| if (safe_arm_volt) { | |
| ret = regulator_set_voltage(arm_regulator, safe_arm_volt, | |
| safe_arm_volt); | |
| if (ret) { | |
| pr_err("%s: failed to set cpu voltage to %d\n", | |
| __func__, safe_arm_volt); | |
| freqs.new = freqs.old; | |
| goto post_notify; | |
| } | |
| } | |
| exynos_info->set_freq(old_index, index); | |
| post_notify: | |
| cpufreq_notify_transition(policy, &freqs, CPUFREQ_POSTCHANGE); | |
| if (ret) | |
| goto out; | |
| /* When the new frequency is lower than current frequency */ | |
| if ((freqs.new < freqs.old) || | |
| ((freqs.new > freqs.old) && safe_arm_volt)) { | |
| /* down the voltage after frequency change */ | |
| regulator_set_voltage(arm_regulator, arm_volt, | |
| arm_volt); | |
| if (ret) { | |
| pr_err("%s: failed to set cpu voltage to %d\n", | |
| __func__, arm_volt); | |
| goto out; | |
| } | |
| } | |
| out: | |
| cpufreq_cpu_put(policy); | |
| return ret; | |
| } | |
| static int exynos_target(struct cpufreq_policy *policy, | |
| unsigned int target_freq, | |
| unsigned int relation) | |
| { | |
| struct cpufreq_frequency_table *freq_table = exynos_info->freq_table; | |
| unsigned int index; | |
| unsigned int new_freq; | |
| int ret = 0; | |
| mutex_lock(&cpufreq_lock); | |
| if (frequency_locked) | |
| goto out; | |
| if (cpufreq_frequency_table_target(policy, freq_table, | |
| target_freq, relation, &index)) { | |
| ret = -EINVAL; | |
| goto out; | |
| } | |
| new_freq = freq_table[index].frequency; | |
| ret = exynos_cpufreq_scale(new_freq); | |
| out: | |
| mutex_unlock(&cpufreq_lock); | |
| return ret; | |
| } | |
| #ifdef CONFIG_PM | |
| static int exynos_cpufreq_suspend(struct cpufreq_policy *policy) | |
| { | |
| return 0; | |
| } | |
| static int exynos_cpufreq_resume(struct cpufreq_policy *policy) | |
| { | |
| return 0; | |
| } | |
| #endif | |
| /** | |
| * exynos_cpufreq_pm_notifier - block CPUFREQ's activities in suspend-resume | |
| * context | |
| * @notifier | |
| * @pm_event | |
| * @v | |
| * | |
| * While frequency_locked == true, target() ignores every frequency but | |
| * locking_frequency. The locking_frequency value is the initial frequency, | |
| * which is set by the bootloader. In order to eliminate possible | |
| * inconsistency in clock values, we save and restore frequencies during | |
| * suspend and resume and block CPUFREQ activities. Note that the standard | |
| * suspend/resume cannot be used as they are too deep (syscore_ops) for | |
| * regulator actions. | |
| */ | |
| static int exynos_cpufreq_pm_notifier(struct notifier_block *notifier, | |
| unsigned long pm_event, void *v) | |
| { | |
| int ret; | |
| switch (pm_event) { | |
| case PM_SUSPEND_PREPARE: | |
| mutex_lock(&cpufreq_lock); | |
| frequency_locked = true; | |
| mutex_unlock(&cpufreq_lock); | |
| ret = exynos_cpufreq_scale(locking_frequency); | |
| if (ret < 0) | |
| return NOTIFY_BAD; | |
| break; | |
| case PM_POST_SUSPEND: | |
| mutex_lock(&cpufreq_lock); | |
| frequency_locked = false; | |
| mutex_unlock(&cpufreq_lock); | |
| break; | |
| } | |
| return NOTIFY_OK; | |
| } | |
| static struct notifier_block exynos_cpufreq_nb = { | |
| .notifier_call = exynos_cpufreq_pm_notifier, | |
| }; | |
| static int exynos_cpufreq_cpu_init(struct cpufreq_policy *policy) | |
| { | |
| policy->cur = policy->min = policy->max = exynos_getspeed(policy->cpu); | |
| cpufreq_frequency_table_get_attr(exynos_info->freq_table, policy->cpu); | |
| /* set the transition latency value */ | |
| policy->cpuinfo.transition_latency = 100000; | |
| cpumask_setall(policy->cpus); | |
| return cpufreq_frequency_table_cpuinfo(policy, exynos_info->freq_table); | |
| } | |
| static int exynos_cpufreq_cpu_exit(struct cpufreq_policy *policy) | |
| { | |
| cpufreq_frequency_table_put_attr(policy->cpu); | |
| return 0; | |
| } | |
| static struct freq_attr *exynos_cpufreq_attr[] = { | |
| &cpufreq_freq_attr_scaling_available_freqs, | |
| NULL, | |
| }; | |
| static struct cpufreq_driver exynos_driver = { | |
| .flags = CPUFREQ_STICKY, | |
| .verify = exynos_verify_speed, | |
| .target = exynos_target, | |
| .get = exynos_getspeed, | |
| .init = exynos_cpufreq_cpu_init, | |
| .exit = exynos_cpufreq_cpu_exit, | |
| .name = "exynos_cpufreq", | |
| .attr = exynos_cpufreq_attr, | |
| #ifdef CONFIG_PM | |
| .suspend = exynos_cpufreq_suspend, | |
| .resume = exynos_cpufreq_resume, | |
| #endif | |
| }; | |
| static int __init exynos_cpufreq_init(void) | |
| { | |
| int ret = -EINVAL; | |
| exynos_info = kzalloc(sizeof(*exynos_info), GFP_KERNEL); | |
| if (!exynos_info) | |
| return -ENOMEM; | |
| if (soc_is_exynos4210()) | |
| ret = exynos4210_cpufreq_init(exynos_info); | |
| else if (soc_is_exynos4212() || soc_is_exynos4412()) | |
| ret = exynos4x12_cpufreq_init(exynos_info); | |
| else if (soc_is_exynos5250()) | |
| ret = exynos5250_cpufreq_init(exynos_info); | |
| else | |
| return 0; | |
| if (ret) | |
| goto err_vdd_arm; | |
| if (exynos_info->set_freq == NULL) { | |
| pr_err("%s: No set_freq function (ERR)\n", __func__); | |
| goto err_vdd_arm; | |
| } | |
| arm_regulator = regulator_get(NULL, "vdd_arm"); | |
| if (IS_ERR(arm_regulator)) { | |
| pr_err("%s: failed to get resource vdd_arm\n", __func__); | |
| goto err_vdd_arm; | |
| } | |
| locking_frequency = exynos_getspeed(0); | |
| register_pm_notifier(&exynos_cpufreq_nb); | |
| if (cpufreq_register_driver(&exynos_driver)) { | |
| pr_err("%s: failed to register cpufreq driver\n", __func__); | |
| goto err_cpufreq; | |
| } | |
| return 0; | |
| err_cpufreq: | |
| unregister_pm_notifier(&exynos_cpufreq_nb); | |
| regulator_put(arm_regulator); | |
| err_vdd_arm: | |
| kfree(exynos_info); | |
| return -EINVAL; | |
| } | |
| late_initcall(exynos_cpufreq_init); |