sdk-hwV1.3/lichee/linux-4.9/drivers/cpufreq/sunxi-cpufreq-pwm.c

430 lines
10 KiB
C

/*
* Copyright (c) 2018 softwinner.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that 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.
*
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cpufreq.h>
#include <linux/cpu.h>
#include <linux/cpu_cooling.h>
#include <linux/of.h>
#include <linux/clk.h>
#include <linux/err.h>
#include <linux/pm_opp.h>
#include <linux/slab.h>
#include <linux/pwm.h>
#include <linux/delay.h>
struct pwm_info_table {
int period_ns;
int vol_base;
int vol_max;
int polarity;
int pwm_id;
};
struct cpufreq_soc_data {
/* If it exists, switch to this clock
* before changing the frequency each time. */
u32 periph_clk;
};
struct cpufreq_pwm_data {
struct pwm_info_table pwm_info;
struct pwm_device *pwm_dev;
struct device *cpu;
struct clk *pll_clk;
struct clk *cpu_clk;
struct clk *periph_clk;
};
static struct thermal_cooling_device *cdev;
static int dvfs_pwm_enale;
#define ENABLE 1
static int pwm_interface_init(struct cpufreq_pwm_data *data)
{
struct device_node *pwm_info_np;
struct pwm_info_table *info = &data->pwm_info;
int rc = 0;
pwm_info_np = of_find_node_by_path("/pwm_dvfs_info");
if (unlikely(!pwm_info_np))
return -ENODEV;
rc = of_property_read_u32(pwm_info_np, "period_ns",
&info->period_ns);
rc |= of_property_read_u32(pwm_info_np, "vol_base",
&info->vol_base);
rc |= of_property_read_u32(pwm_info_np, "vol_max",
&info->vol_max);
rc |= of_property_read_u32(pwm_info_np, "polarity",
&info->polarity);
rc |= of_property_read_u32(pwm_info_np, "pwm_id",
&info->pwm_id);
if (rc)
goto out;
data->pwm_dev = pwm_request(info->pwm_id, "pwm-cpu");
if (IS_ERR(data->pwm_dev)) {
rc = PTR_ERR(data->pwm_dev);
goto out;
}
rc = pwm_config(data->pwm_dev, info->period_ns, info->period_ns);
if (rc)
goto out;
rc = pwm_set_polarity(data->pwm_dev, info->polarity);
if (rc)
goto out;
rc = pwm_enable(data->pwm_dev);
if (rc)
goto out;
out:
of_node_put(pwm_info_np);
return rc;
}
static int sunxi_set_cpufreq(struct cpufreq_policy *policy,
unsigned int freq)
{
struct device *cpu = get_cpu_device(policy->cpu);
struct cpufreq_pwm_data *data =
(struct cpufreq_pwm_data *)(policy->driver_data);
int rc;
if (!data->periph_clk) {
rc = dev_pm_opp_set_rate(cpu, freq * 1000);
if (rc)
pr_err("Set cpu frequency to %dKHz failed!\n", freq);
goto out;
}
/* Reparent the CPU clock */
rc = clk_set_parent(data->cpu_clk, data->periph_clk);
if (WARN_ON(rc)) {
pr_err("cpu%d: failed to re-parent cpu clock!\n",
policy->cpu);
goto out;
}
/* Set the original PLL to target rate */
rc = clk_set_rate(data->pll_clk, freq * 1000);
if (rc) {
pr_err("cpu%d: failed to scale cpu clock rate!\n",
policy->cpu);
clk_set_parent(data->cpu_clk, data->pll_clk);
goto out;
}
/* Set parent of CPU clock back to the original PLL */
rc = clk_set_parent(data->cpu_clk, data->pll_clk);
if (WARN_ON(rc)) {
pr_err("cpu%d: failed to re-parent cpu clock!\n",
policy->cpu);
goto out;
}
out:
return rc;
}
static int sunxi_set_cpuvol(struct cpufreq_policy *policy,
unsigned int freq)
{
struct cpufreq_pwm_data *data =
(struct cpufreq_pwm_data *)(policy->driver_data);
struct pwm_info_table *pwm_info = &data->pwm_info;
struct device *cpu = get_cpu_device(policy->cpu);
unsigned long volt, freq_hz = freq * 1000;
struct dev_pm_opp *opp;
long duty_ns;
int rc = 0;
rcu_read_lock();
opp = dev_pm_opp_find_freq_ceil(cpu, &freq_hz);
if (IS_ERR(opp)) {
rcu_read_unlock();
pr_err("failed to find OPP for %ld\n", freq_hz);
return PTR_ERR(opp);
}
volt = dev_pm_opp_get_voltage(opp);
rcu_read_unlock();
if (volt <= pwm_info->vol_base)
duty_ns = 0;
else if (volt >= pwm_info->vol_max)
duty_ns = pwm_info->period_ns;
else if (pwm_info->vol_base < volt && volt < pwm_info->vol_max) {
/* Div 1000 for convert to mV */
duty_ns = ((volt - pwm_info->vol_base) / 1000)
* pwm_info->period_ns
/ ((pwm_info->vol_max - pwm_info->vol_base) / 1000);
}
pr_info("duty_ns:%ld\tperiod_ns:%d\n", duty_ns, pwm_info->period_ns);
rc = pwm_config(data->pwm_dev, duty_ns, pwm_info->period_ns);
if (rc)
pr_err("Set cpu voltage to %lduV failed!\n", volt);
return rc;
}
static int sunxi_cpufreq_target_index(struct cpufreq_policy *policy,
unsigned int index)
{
unsigned long freq = policy->freq_table[index].frequency;
unsigned long old_freq = clk_get_rate(policy->clk) / 1000;
int rc = 0;
if (freq > old_freq) {
/* Add vol at first */
if (dvfs_pwm_enale) {
rc = sunxi_set_cpuvol(policy, freq);
usleep_range(300, 500);
if (rc)
goto out;
}
rc = sunxi_set_cpufreq(policy, freq);
if (rc)
goto out;
} else if (freq < old_freq) {
/* Decrease freq at first */
rc = sunxi_set_cpufreq(policy, freq);
if (rc)
goto out;
if (dvfs_pwm_enale) {
rc = sunxi_set_cpuvol(policy, freq);
if (rc)
goto out;
}
}
out:
return rc;
}
static const struct cpufreq_soc_data sun8i_18_data = {
.periph_clk = 1,
};
static const struct of_device_id sunxi_cpufreq_match_list[] = {
{ .compatible = "allwinner,sun8iw18p1", .data = &sun8i_18_data },
{}
};
static const struct of_device_id *sunxi_cpufreq_match_node(void)
{
struct device_node *np;
const struct of_device_id *match;
np = of_find_node_by_path("/");
match = of_match_node(sunxi_cpufreq_match_list, np);
of_node_put(np);
return match;
}
static int sunxi_cpufreq_init(struct cpufreq_policy *policy)
{
struct device *cpu = get_cpu_device(policy->cpu);
const struct cpufreq_soc_data *soc_data;
struct cpufreq_frequency_table *freq_table;
struct device_node *dvfs_table_np;
struct cpufreq_pwm_data *pwm_data;
const struct of_device_id *match;
int transition_latency;
int ret;
dvfs_table_np = of_find_node_by_path("/opp_l_table0");
if (!dvfs_table_np)
return -ENODEV;
of_node_put(dvfs_table_np);
pwm_data = kzalloc(sizeof(*pwm_data), GFP_KERNEL);
if (!pwm_data)
return -ENOMEM;
pwm_data->pll_clk = of_clk_get(cpu->of_node, 0);
if (IS_ERR_OR_NULL(pwm_data->pll_clk)) {
ret = PTR_ERR(pwm_data->pll_clk);
goto out_free;
}
match = sunxi_cpufreq_match_node();
if (!match) {
ret = -ENODEV;
goto out_put_clk;
}
soc_data = match->data;
if (!soc_data) {
ret = -EINVAL;
goto out_put_clk;
}
if (soc_data->periph_clk) {
pwm_data->cpu_clk = of_clk_get(cpu->of_node, 1);
if (IS_ERR_OR_NULL(pwm_data->cpu_clk)) {
ret = PTR_ERR(pwm_data->cpu_clk);
goto out_put_clk;
}
pwm_data->periph_clk = of_clk_get(cpu->of_node, 2);
if (IS_ERR_OR_NULL(pwm_data->periph_clk)) {
ret = PTR_ERR(pwm_data->periph_clk);
goto out_put_cpu_clk;
}
}
/* Get OPP-sharing information from "operating-points-v2" bindings */
ret = dev_pm_opp_of_get_sharing_cpus(cpu, policy->cpus);
if (ret)
goto out_put_periph_clk;
dev_pm_opp_of_cpumask_add_table(policy->cpus);
ret = dev_pm_opp_get_opp_count(cpu);
if (ret <= 0) {
pr_info("OPP table is not ready, deferring probe\n");
ret = -EPROBE_DEFER;
goto out_free_opp;
}
ret = dev_pm_opp_init_cpufreq_table(cpu, &freq_table);
if (ret)
goto out_free_opp;
ret = cpufreq_table_validate_and_show(policy, freq_table);
if (ret)
goto out_free_opp;
transition_latency = dev_pm_opp_get_max_transition_latency(cpu);
if (!transition_latency)
transition_latency = CPUFREQ_ETERNAL;
policy->cpuinfo.transition_latency = transition_latency;
policy->clk = pwm_data->pll_clk;
policy->driver_data = pwm_data;
ret = pwm_interface_init(pwm_data);
if (!ret)
dvfs_pwm_enale = ENABLE;
return 0;
out_free_opp:
dev_pm_opp_free_cpufreq_table(cpu, &policy->freq_table);
dev_pm_opp_of_cpumask_remove_table(policy->cpus);
out_put_periph_clk:
if (pwm_data->periph_clk)
clk_put(pwm_data->periph_clk);
out_put_cpu_clk:
if (pwm_data->cpu_clk)
clk_put(pwm_data->cpu_clk);
out_put_clk:
clk_put(pwm_data->pll_clk);
out_free:
kfree(pwm_data);
return ret;
}
static int sunxi_cpufreq_exit(struct cpufreq_policy *policy)
{
struct cpufreq_pwm_data *pwm_data = policy->driver_data;
struct device *cpu;
cpu = get_cpu_device(policy->cpu);
clk_put(policy->clk);
if (pwm_data->cpu_clk)
clk_put(pwm_data->cpu_clk);
if (pwm_data->periph_clk)
clk_put(pwm_data->periph_clk);
kfree(pwm_data);
cpufreq_cooling_unregister(cdev);
dev_pm_opp_free_cpufreq_table(cpu, &policy->freq_table);
dev_pm_opp_of_cpumask_remove_table(policy->related_cpus);
return 0;
}
static void sunxi_cpufreq_ready(struct cpufreq_policy *policy)
{
struct device *cpu = get_cpu_device(policy->cpu);
struct device_node *np;
np = of_node_get(cpu->of_node);
if (WARN_ON(!np))
return;
if (of_find_property(np, "#cooling-cells", NULL)) {
u32 power_coefficient = 0;
of_property_read_u32(np, "dynamic-power-coefficient",
&power_coefficient);
cdev = of_cpufreq_power_cooling_register(np,
policy->related_cpus, power_coefficient, NULL);
if (IS_ERR(cdev)) {
dev_err(cpu,
"running cpufreq without cooling device: %ld\n",
PTR_ERR(cdev));
cdev = NULL;
}
}
of_node_put(np);
}
static struct freq_attr *sunxi_cpufreq_attr[] = {
&cpufreq_freq_attr_scaling_available_freqs,
NULL,
};
static struct cpufreq_driver sunxi_cpufreq_driver = {
.name = "cpufreq-pwm",
.flags = CPUFREQ_STICKY | CPUFREQ_NEED_INITIAL_FREQ_CHECK,
.attr = sunxi_cpufreq_attr,
.init = sunxi_cpufreq_init,
.get = cpufreq_generic_get,
.target_index = sunxi_cpufreq_target_index,
.exit = sunxi_cpufreq_exit,
.ready = sunxi_cpufreq_ready,
.verify = cpufreq_generic_frequency_table_verify,
};
static int __init sunxi_cpufreq_initcall(void)
{
int ret;
ret = cpufreq_register_driver(&sunxi_cpufreq_driver);
if (ret)
pr_err("Failed register driver\n");
return ret;
}
module_init(sunxi_cpufreq_initcall);
MODULE_AUTHOR("frank <frank@allwinnertech.com>");
MODULE_DESCRIPTION("SUNXI DVFS BY PWM");
MODULE_LICENSE("GPL");