| /* |
| * Copyright (c) 2015 NVIDIA CORPORATION. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify it |
| * under the terms and conditions of the GNU General Public License, |
| * version 2, as published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope 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. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| */ |
| |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
| |
| #include <linux/clk.h> |
| #include <linux/clk-provider.h> |
| #include <linux/cpufreq.h> |
| #include <linux/cpuquiet.h> |
| #include <linux/err.h> |
| #include <linux/init.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/of_device.h> |
| #include <linux/of.h> |
| #include <linux/platform_device.h> |
| #include <linux/pm_opp.h> |
| #include <linux/regulator/consumer.h> |
| #include <linux/types.h> |
| #include <linux/clk/tegra.h> |
| #include <soc/tegra/tegra-dvfs.h> |
| |
| #define TEGRA210_CPU_BACKUP_RATE 204000000U |
| |
| static DEFINE_MUTEX(tegra_cpu_lock); |
| |
| struct tegra210_cpufreq_priv { |
| struct regulator *vdd_cpu_reg; |
| struct clk *cpu_g_clk; |
| struct clk *cpu_lp_clk; |
| struct clk *pllp_clk; |
| struct clk *pllx_clk; |
| struct clk *dfll_clk; |
| struct clk *cpu_clk; |
| struct clk *emc_clk; |
| struct workqueue_struct *emc_rate_wq; |
| struct work_struct emc_rate_work; |
| struct device *cpu_dev; |
| int suspend_index; |
| bool dfll_mode; |
| }; |
| |
| static struct tegra210_cpufreq_priv priv; |
| |
| static int tegra210_cpu_switch_to_dfll(void) |
| { |
| int ret; |
| |
| ret = clk_set_rate(priv.dfll_clk, clk_get_rate(priv.cpu_clk)); |
| if (ret) |
| return ret; |
| |
| clk_set_parent(priv.cpu_clk, priv.dfll_clk); |
| tegra_dvfs_dfll_mode_set(priv.cpu_clk, |
| clk_get_rate(priv.dfll_clk)); |
| priv.dfll_mode = true; |
| |
| return 0; |
| } |
| |
| static void tegra210_cpu_switch_to_pllx(void) |
| { |
| clk_set_parent(priv.cpu_clk, priv.pllp_clk); |
| regulator_sync_voltage(priv.vdd_cpu_reg); |
| clk_set_parent(priv.cpu_clk, priv.pllx_clk); |
| tegra_dvfs_dfll_mode_clear(priv.cpu_clk, |
| clk_get_rate(priv.pllx_clk)); |
| priv.dfll_mode = false; |
| } |
| |
| static int tegra210_set_rate_pll(unsigned int new_rate, unsigned int old_rate) |
| { |
| int ret = 0; |
| bool dramp = false; |
| unsigned long backup_rate, main_rate; |
| |
| if (clk_get_parent(priv.cpu_clk) == priv.pllx_clk) { |
| main_rate = new_rate; |
| |
| dramp = (new_rate > TEGRA210_CPU_BACKUP_RATE) && |
| tegra_pll_can_ramp_to_rate(priv.pllx_clk, new_rate); |
| |
| if (dramp || |
| tegra_pll_can_ramp_to_min(priv.pllx_clk, &main_rate)) { |
| ret = clk_set_rate(priv.pllx_clk, main_rate); |
| if (ret) { |
| pr_err("%s: Failed to set rate %lu on %s\n", |
| __func__, main_rate, |
| __clk_get_name(priv.pllx_clk)); |
| goto out; |
| } |
| |
| if (main_rate == new_rate) |
| goto out; |
| } else { |
| pr_err("%s: not ready for dynamic ramp to %u\n", |
| __clk_get_name(priv.pllx_clk), new_rate); |
| } |
| |
| /* |
| * Switch to back-up source, and stay on it if target rate is |
| * below backup rate |
| */ |
| ret = clk_set_parent(priv.cpu_clk, priv.pllp_clk); |
| if (ret) { |
| pr_err("%s: Failed to switch cpu to %s\n", |
| __func__, __clk_get_name(priv.pllp_clk)); |
| goto out; |
| } |
| } |
| |
| backup_rate = min(new_rate, TEGRA210_CPU_BACKUP_RATE); |
| if (backup_rate != clk_get_rate(priv.cpu_clk)) { |
| ret = clk_set_rate(priv.pllp_clk, backup_rate); |
| if (ret) { |
| pr_err("%s: Failed to set rate %lu on backup source\n", |
| __func__, backup_rate); |
| goto out; |
| } |
| } |
| if (new_rate == backup_rate) |
| goto out; |
| |
| /* |
| * Switch from backup source to main at rate not exceeding pll VCO |
| * minimum. Use dynamic ramp to reach target rate if it is above VCO |
| * minimum. |
| */ |
| main_rate = new_rate; |
| if (tegra_pll_get_min_ramp_rate(priv.pllx_clk, new_rate, &main_rate)) { |
| pr_err("%s: not ready for dynamic ramp to %u\n", |
| __clk_get_name(priv.pllx_clk), new_rate); |
| } |
| |
| ret = clk_set_rate(priv.pllx_clk, main_rate); |
| if (ret) { |
| pr_err("%s: Failed to set cpu rate %lu on source %s\n", |
| __func__, main_rate, __clk_get_name(priv.pllx_clk)); |
| goto out; |
| } |
| |
| ret = clk_set_parent(priv.cpu_clk, priv.pllx_clk); |
| if (ret) { |
| pr_err("%s: Failed to switch cpu to %s\n", |
| __func__, __clk_get_name(priv.pllx_clk)); |
| goto out; |
| } |
| |
| if (new_rate != main_rate) { |
| ret = clk_set_rate(priv.pllx_clk, new_rate); |
| if (ret) { |
| pr_err("%s: Failed to set cpu rate %u on source%s\n", |
| __func__, new_rate, |
| __clk_get_name(priv.pllx_clk)); |
| goto out; |
| } |
| } |
| |
| return 0; |
| out: |
| return ret; |
| } |
| |
| static void tegra210_vote_emc_rate_work(struct work_struct *work) |
| { |
| unsigned long cpu_rate; |
| |
| /* Vote on memory bus frequency based on cpu frequency */ |
| cpu_rate = clk_get_rate(priv.cpu_clk); |
| if (cpu_rate >= 1300000000) |
| /* cpu >= 1.3GHz, emc max */ |
| clk_set_rate(priv.emc_clk, ULONG_MAX); |
| else if (cpu_rate >= 975000000) |
| /* cpu >= 975 MHz, emc 400 MHz */ |
| clk_set_rate(priv.emc_clk, 400000000); |
| else if (cpu_rate >= 725000000) |
| /* cpu >= 725 MHz, emc 200 MHz */ |
| clk_set_rate(priv.emc_clk, 200000000); |
| else if (cpu_rate >= 500000000) |
| /* cpu >= 500 MHz, emc 100 MHz */ |
| clk_set_rate(priv.emc_clk, 100000000); |
| else if (cpu_rate >= 275000000) |
| /* cpu >= 275 MHz, emc 50 MHz */ |
| clk_set_rate(priv.emc_clk, 50000000); |
| else |
| /* emc min */ |
| clk_set_rate(priv.emc_clk, 0); |
| } |
| |
| static int tegra210_set_target(struct cpufreq_policy *policy, |
| unsigned int index) |
| { |
| struct cpufreq_frequency_table *freq_table = policy->freq_table; |
| unsigned int old_rate, new_rate; |
| |
| mutex_lock(&tegra_cpu_lock); |
| |
| new_rate = clk_round_rate(priv.cpu_clk, freq_table[index].frequency * 1000); |
| old_rate = clk_get_rate(priv.cpu_clk); |
| |
| if (priv.dfll_mode) |
| clk_set_rate(priv.cpu_clk, new_rate); |
| else |
| tegra210_set_rate_pll(new_rate, old_rate); |
| |
| if (new_rate != old_rate) |
| queue_work(priv.emc_rate_wq, &priv.emc_rate_work); |
| |
| mutex_unlock(&tegra_cpu_lock); |
| |
| return 0; |
| } |
| |
| static int tegra210_cpufreq_init(struct cpufreq_policy *policy) |
| { |
| struct cpufreq_frequency_table *freq_table; |
| struct device_node *np; |
| unsigned int transition_latency; |
| int ret; |
| |
| np = of_cpu_device_node_get(0); |
| if (!np) |
| return -ENODEV; |
| |
| of_init_opp_table(priv.cpu_dev); |
| |
| if (of_property_read_u32(np, "clock-latency", &transition_latency)) |
| transition_latency = CPUFREQ_ETERNAL; |
| |
| ret = dev_pm_opp_init_cpufreq_table(priv.cpu_dev, &freq_table); |
| if (ret) { |
| pr_err("failed to init cpufreq table: %d\n", ret); |
| goto out_put_node; |
| } |
| |
| policy->clk = priv.cpu_clk; |
| policy->cpuinfo.transition_latency = transition_latency; |
| policy->suspend_freq = freq_table[priv.suspend_index].frequency; |
| cpumask_setall(policy->cpus); |
| |
| ret = cpufreq_table_validate_and_show(policy, freq_table); |
| if (ret) { |
| dev_err(priv.cpu_dev, "%s: invalid frequency table: %d\n", |
| __func__, ret); |
| goto out_unregister; |
| } |
| |
| of_node_put(np); |
| |
| return 0; |
| out_unregister: |
| dev_pm_opp_free_cpufreq_table(priv.cpu_dev, &freq_table); |
| out_put_node: |
| of_node_put(np); |
| |
| return ret; |
| } |
| |
| static int tegra210_cpufreq_exit(struct cpufreq_policy *policy) |
| { |
| dev_pm_opp_free_cpufreq_table(priv.cpu_dev, &policy->freq_table); |
| |
| return 0; |
| } |
| |
| static struct cpufreq_driver tegra210_cpufreq_driver = { |
| .flags = CPUFREQ_STICKY | CPUFREQ_NEED_INITIAL_FREQ_CHECK, |
| .verify = cpufreq_generic_frequency_table_verify, |
| .target_index = tegra210_set_target, |
| .get = cpufreq_generic_get, |
| .init = tegra210_cpufreq_init, |
| .exit = tegra210_cpufreq_exit, |
| .name = "cpufreq-tegra210", |
| .attr = cpufreq_generic_attr, |
| }; |
| |
| static int tegra210_cpufreq_probe(struct platform_device *pdev) |
| { |
| struct device_node *np; |
| struct device *cpu_dev; |
| int ret; |
| |
| cpu_dev = get_cpu_device(0); |
| if (!cpu_dev) |
| return -ENODEV; |
| |
| priv.cpu_dev = cpu_dev; |
| |
| np = of_cpu_device_node_get(0); |
| if (!np) |
| return -ENODEV; |
| |
| priv.vdd_cpu_reg = regulator_get(cpu_dev, "vdd-cpu"); |
| if (IS_ERR(priv.vdd_cpu_reg)) { |
| ret = PTR_ERR(priv.vdd_cpu_reg); |
| goto out_put_np; |
| } |
| |
| priv.cpu_clk = priv.cpu_g_clk = of_clk_get_by_name(np, "cpu_g"); |
| if (IS_ERR(priv.cpu_g_clk)) { |
| ret = PTR_ERR(priv.cpu_g_clk); |
| goto out_put_vdd_cpu_reg; |
| } |
| |
| priv.cpu_lp_clk = of_clk_get_by_name(np, "cpu_lp"); |
| if (IS_ERR(priv.cpu_lp_clk)) { |
| ret = PTR_ERR(priv.cpu_lp_clk); |
| goto out_put_cpu_clk; |
| } |
| |
| priv.dfll_clk = of_clk_get_by_name(np, "dfll"); |
| if (IS_ERR(priv.dfll_clk)) { |
| ret = PTR_ERR(priv.dfll_clk); |
| goto out_put_cpu_lp_clk; |
| } |
| |
| priv.pllx_clk = of_clk_get_by_name(np, "pll_x"); |
| if (IS_ERR(priv.pllx_clk)) { |
| ret = PTR_ERR(priv.pllx_clk); |
| goto out_put_dfll_clk; |
| } |
| |
| priv.pllp_clk = of_clk_get_by_name(np, "pll_p"); |
| if (IS_ERR(priv.pllp_clk)) { |
| ret = PTR_ERR(priv.pllp_clk); |
| goto out_put_pllx_clk; |
| } |
| |
| priv.emc_clk = of_clk_get_by_name(np, "emc"); |
| if (IS_ERR(priv.emc_clk)) { |
| pr_info("Tegra210_cpufreq: no cpu.emc shared clock found\n"); |
| priv.emc_clk = NULL; |
| } |
| clk_prepare_enable(priv.emc_clk); |
| |
| priv.emc_rate_wq = alloc_workqueue("emc_rate_wq", |
| WQ_HIGHPRI | WQ_UNBOUND, 1); |
| if (!priv.emc_rate_wq) { |
| ret = -ENOMEM; |
| goto out_put_emc_clk; |
| } |
| INIT_WORK(&priv.emc_rate_work, tegra210_vote_emc_rate_work); |
| |
| rcu_read_lock(); |
| ret = dev_pm_opp_get_opp_count(cpu_dev); |
| rcu_read_unlock(); |
| if (ret <= 0) { |
| pr_debug("OPP table is not ready, deferring probe\n"); |
| ret = -EPROBE_DEFER; |
| goto out_put_emc_clk; |
| } |
| priv.suspend_index = ret - 1; |
| |
| ret = tegra210_cpu_switch_to_dfll(); |
| if (ret) |
| goto out_put_pllp_clk; |
| |
| ret = cpufreq_register_driver(&tegra210_cpufreq_driver); |
| if (ret) { |
| dev_err(priv.cpu_dev, "failed register driver: %d\n", ret); |
| goto out_switch_pllx; |
| } |
| |
| return 0; |
| |
| out_switch_pllx: |
| tegra210_cpu_switch_to_pllx(); |
| out_put_emc_clk: |
| if (priv.emc_rate_wq) |
| destroy_workqueue(priv.emc_rate_wq); |
| clk_disable_unprepare(priv.emc_clk); |
| clk_put(priv.emc_clk); |
| out_put_pllp_clk: |
| clk_put(priv.pllp_clk); |
| out_put_pllx_clk: |
| clk_put(priv.pllx_clk); |
| out_put_dfll_clk: |
| clk_put(priv.dfll_clk); |
| out_put_cpu_lp_clk: |
| clk_put(priv.cpu_lp_clk); |
| out_put_cpu_clk: |
| clk_put(priv.cpu_clk); |
| out_put_vdd_cpu_reg: |
| regulator_put(priv.vdd_cpu_reg); |
| out_put_np: |
| of_node_put(np); |
| |
| return ret; |
| } |
| |
| static int tegra210_cpufreq_remove(struct platform_device *pdev) |
| { |
| tegra210_cpu_switch_to_pllx(); |
| |
| cancel_work_sync(&priv.emc_rate_work); |
| destroy_workqueue(priv.emc_rate_wq); |
| clk_disable_unprepare(priv.emc_clk); |
| clk_put(priv.emc_clk); |
| clk_put(priv.pllp_clk); |
| clk_put(priv.pllx_clk); |
| clk_put(priv.dfll_clk); |
| clk_put(priv.cpu_clk); |
| regulator_put(priv.vdd_cpu_reg); |
| |
| return 0; |
| } |
| |
| static struct platform_driver tegra210_cpufreq_platdrv = { |
| .driver = { |
| .name = "cpufreq-tegra210", |
| }, |
| .probe = tegra210_cpufreq_probe, |
| .remove = tegra210_cpufreq_remove, |
| }; |
| |
| static const struct cpuquiet_platform_info tegra_plat_info = { |
| .plat_name = "tegra", |
| .avg_hotplug_latency_ms = 2, |
| }; |
| |
| static int __init tegra_cpufreq_init(void) |
| { |
| int ret; |
| struct platform_device *pdev; |
| |
| if (!of_machine_is_compatible("nvidia,tegra210")) |
| return -ENODEV; |
| |
| ret = platform_driver_register(&tegra210_cpufreq_platdrv); |
| if (ret) |
| return ret; |
| |
| pdev = platform_device_register_simple("cpufreq-tegra210", -1, NULL, 0); |
| if (IS_ERR(pdev)) { |
| platform_driver_unregister(&tegra210_cpufreq_platdrv); |
| return PTR_ERR(pdev); |
| } |
| |
| cpuquiet_init(&tegra_plat_info); |
| |
| return 0; |
| } |
| module_init(tegra_cpufreq_init); |
| |
| MODULE_AUTHOR("Penny Chiu <pchiu@nvidia.com>"); |
| MODULE_DESCRIPTION("cpufreq driver for NVIDIA tegra210"); |
| MODULE_LICENSE("GPL v2"); |