2025-05-10 21:49:39 +08:00

557 lines
13 KiB
C

/*
* Rockchip CPUFreq Driver
*
* Copyright (C) 2017 Fuzhou Rockchip Electronics Co., Ltd
*
* 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.
*
* This program is distributed "as is" WITHOUT ANY WARRANTY of any
* kind, whether express or implied; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include <linux/clk.h>
#include <linux/cpu.h>
#include <linux/cpufreq.h>
#include <linux/err.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/nvmem-consumer.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/pm_opp.h>
#include <linux/regulator/consumer.h>
#include <linux/slab.h>
#include <linux/soc/rockchip/pvtm.h>
#include <linux/thermal.h>
#include <linux/rockchip/cpu.h>
#include <soc/rockchip/rockchip_opp_select.h>
#include <soc/rockchip/rockchip_system_monitor.h>
#include "cpufreq-dt.h"
#include "rockchip-cpufreq.h"
#include "../clk/rockchip/clk.h"
#define LEAKAGE_INVALID 0xff
struct cluster_info {
struct opp_table *opp_table;
struct list_head list_head;
cpumask_t cpus;
unsigned int reboot_freq;
unsigned int threshold_freq;
unsigned int scale_rate;
unsigned int temp_limit_rate;
int volt_sel;
int scale;
int process;
bool offline;
bool freq_limit;
bool is_check_init;
};
static LIST_HEAD(cluster_info_list);
static __maybe_unused int px30_get_soc_info(struct device *dev,
struct device_node *np,
int *bin, int *process)
{
int ret = 0;
u8 value = 0;
if (!bin)
return 0;
if (of_property_match_string(np, "nvmem-cell-names",
"performance") >= 0) {
ret = rockchip_nvmem_cell_read_u8(np, "performance", &value);
if (ret) {
dev_err(dev, "Failed to get soc performance value\n");
return ret;
}
*bin = value;
}
if (*bin >= 0)
dev_info(dev, "bin=%d\n", *bin);
return ret;
}
static __maybe_unused int rk3288_get_soc_info(struct device *dev,
struct device_node *np,
int *bin, int *process)
{
int ret = 0;
u8 value = 0;
char *name;
if (!bin)
goto next;
if (of_property_match_string(np, "nvmem-cell-names", "special") >= 0) {
ret = rockchip_nvmem_cell_read_u8(np, "special", &value);
if (ret) {
dev_err(dev, "Failed to get soc special value\n");
goto out;
}
if (value == 0xc)
*bin = 0;
else
*bin = 1;
}
if (soc_is_rk3288w())
name = "performance-w";
else
name = "performance";
if (of_property_match_string(np, "nvmem-cell-names", name) >= 0) {
ret = rockchip_nvmem_cell_read_u8(np, name, &value);
if (ret) {
dev_err(dev, "Failed to get soc performance value\n");
goto out;
}
if (value & 0x2)
*bin = 3;
else if (value & 0x01)
*bin = 2;
}
if (*bin >= 0)
dev_info(dev, "bin=%d\n", *bin);
next:
if (!process)
goto out;
if (of_property_match_string(np, "nvmem-cell-names",
"process") >= 0) {
ret = rockchip_nvmem_cell_read_u8(np, "process", &value);
if (ret) {
dev_err(dev, "Failed to get soc process version\n");
goto out;
}
if (soc_is_rk3288() && (value == 0 || value == 1))
*process = 0;
}
if (*process >= 0)
dev_info(dev, "process=%d\n", *process);
out:
return ret;
}
static __maybe_unused int rk3399_get_soc_info(struct device *dev,
struct device_node *np,
int *bin, int *process)
{
int ret = 0;
u8 value = 0;
if (!bin)
return 0;
if (of_property_match_string(np, "nvmem-cell-names",
"specification_serial_number") >= 0) {
ret = rockchip_nvmem_cell_read_u8(np,
"specification_serial_number",
&value);
if (ret) {
dev_err(dev,
"Failed to get specification_serial_number\n");
goto out;
}
if (value == 0xb) {
*bin = 0;
} else if (value == 0x1) {
if (of_property_match_string(np, "nvmem-cell-names",
"customer_demand") >= 0) {
ret = rockchip_nvmem_cell_read_u8(np,
"customer_demand",
&value);
if (ret) {
dev_err(dev, "Failed to get customer_demand\n");
goto out;
}
if (value == 0x0)
*bin = 0;
else
*bin = 1;
}
} else if (value == 0x10) {
*bin = 1;
}
}
out:
if (*bin >= 0)
dev_info(dev, "bin=%d\n", *bin);
return ret;
}
static __maybe_unused int rv1126_get_soc_info(struct device *dev,
struct device_node *np,
int *bin, int *process)
{
int ret = 0;
u8 value = 0;
if (of_property_match_string(np, "nvmem-cell-names", "performance") >= 0) {
ret = rockchip_nvmem_cell_read_u8(np, "performance", &value);
if (ret) {
dev_err(dev, "Failed to get soc performance value\n");
return ret;
}
if (value == 0x1)
*bin = 1;
else
*bin = 0;
}
if (*bin >= 0)
dev_info(dev, "bin=%d\n", *bin);
return ret;
}
static const struct of_device_id rockchip_cpufreq_of_match[] = {
#ifdef CONFIG_CPU_PX30
{
.compatible = "rockchip,px30",
.data = (void *)&px30_get_soc_info,
},
#endif
#ifdef CONFIG_CPU_RK3288
{
.compatible = "rockchip,rk3288",
.data = (void *)&rk3288_get_soc_info,
},
{
.compatible = "rockchip,rk3288w",
.data = (void *)&rk3288_get_soc_info,
},
#endif
#ifdef CONFIG_CPU_PX30
{
.compatible = "rockchip,rk3326",
.data = (void *)&px30_get_soc_info,
},
#endif
#ifdef CONFIG_CPU_RK3399
{
.compatible = "rockchip,rk3399",
.data = (void *)&rk3399_get_soc_info,
},
#endif
#ifdef CONFIG_CPU_RV1126
{
.compatible = "rockchip,rv1109",
.data = (void *)&rv1126_get_soc_info,
},
{
.compatible = "rockchip,rv1126",
.data = (void *)&rv1126_get_soc_info,
},
#endif
{},
};
static struct cluster_info *rockchip_cluster_info_lookup(int cpu)
{
struct cluster_info *cluster;
list_for_each_entry(cluster, &cluster_info_list, list_head) {
if (cpumask_test_cpu(cpu, &cluster->cpus))
return cluster;
}
return NULL;
}
static struct cluster_info *rockchip_cluster_lookup_by_dev(struct device *dev)
{
struct cluster_info *cluster;
struct device *cpu_dev;
int cpu;
list_for_each_entry(cluster, &cluster_info_list, list_head) {
for_each_cpu(cpu, &cluster->cpus) {
cpu_dev = get_cpu_device(cpu);
if (!cpu_dev)
continue;
if (cpu_dev == dev)
return cluster;
}
}
return NULL;
}
static int rockchip_cpufreq_cluster_init(int cpu, struct cluster_info *cluster)
{
struct device_node *np;
struct property *pp;
struct device *dev;
char *reg_name = NULL;
int ret = 0, bin = -EINVAL;
cluster->process = -EINVAL;
cluster->volt_sel = -EINVAL;
cluster->scale = 0;
dev = get_cpu_device(cpu);
if (!dev)
return -ENODEV;
pp = of_find_property(dev->of_node, "cpu-supply", NULL);
if (pp) {
reg_name = "cpu";
} else {
pp = of_find_property(dev->of_node, "cpu0-supply", NULL);
if (pp)
reg_name = "cpu0";
else
return -ENOENT;
}
np = of_parse_phandle(dev->of_node, "operating-points-v2", 0);
if (!np) {
dev_warn(dev, "OPP-v2 not supported\n");
return -ENOENT;
}
ret = dev_pm_opp_of_get_sharing_cpus(dev, &cluster->cpus);
if (ret) {
dev_err(dev, "Failed to get sharing cpus\n");
goto np_err;
}
of_property_read_u32(np, "rockchip,threshold-freq",
&cluster->threshold_freq);
cluster->freq_limit = of_property_read_bool(np, "rockchip,freq-limit");
rockchip_get_soc_info(dev, rockchip_cpufreq_of_match,
&bin, &cluster->process);
rockchip_get_scale_volt_sel(dev, "cpu_leakage", reg_name,
bin, cluster->process,
&cluster->scale, &cluster->volt_sel);
rockchip_set_opp_supported_hw(dev, np, bin, cluster->volt_sel);
np_err:
of_node_put(np);
return ret;
}
int rockchip_cpufreq_check_rate_volt(struct device *dev)
{
struct cluster_info *cluster;
cluster = rockchip_cluster_lookup_by_dev(dev);
if (!cluster)
return -EINVAL;
if (cluster->is_check_init)
return 0;
dev_pm_opp_check_rate_volt(dev, true);
cluster->is_check_init = true;
return 0;
}
EXPORT_SYMBOL_GPL(rockchip_cpufreq_check_rate_volt);
int rockchip_cpufreq_set_opp_info(struct device *dev)
{
struct cluster_info *cluster;
cluster = rockchip_cluster_lookup_by_dev(dev);
if (!cluster)
return -EINVAL;
cluster->opp_table = rockchip_set_opp_prop_name(dev,
cluster->process,
cluster->volt_sel);
if (IS_ERR(cluster->opp_table)) {
dev_err(dev, "Failed to set prop name\n");
return PTR_ERR(cluster->opp_table);
}
return 0;
}
EXPORT_SYMBOL_GPL(rockchip_cpufreq_set_opp_info);
void rockchip_cpufreq_put_opp_info(struct device *dev)
{
struct cluster_info *cluster;
cluster = rockchip_cluster_lookup_by_dev(dev);
if (!cluster)
return;
if (!IS_ERR_OR_NULL(cluster->opp_table))
dev_pm_opp_put_prop_name(cluster->opp_table);
}
EXPORT_SYMBOL_GPL(rockchip_cpufreq_put_opp_info);
int rockchip_cpufreq_adjust_power_scale(struct device *dev)
{
struct cluster_info *cluster;
cluster = rockchip_cluster_lookup_by_dev(dev);
if (!cluster)
return -EINVAL;
rockchip_adjust_power_scale(dev, cluster->scale);
return 0;
}
EXPORT_SYMBOL_GPL(rockchip_cpufreq_adjust_power_scale);
int rockchip_cpufreq_suspend(struct cpufreq_policy *policy)
{
int ret = 0;
ret = cpufreq_generic_suspend(policy);
if (!ret)
rockchip_monitor_suspend_low_temp_adjust(policy->cpu);
return ret;
}
EXPORT_SYMBOL_GPL(rockchip_cpufreq_suspend);
static struct cpufreq_policy *rockchip_get_policy(struct cluster_info *cluster)
{
int first_cpu;
first_cpu = cpumask_first_and(&cluster->cpus, cpu_online_mask);
if (first_cpu >= nr_cpu_ids)
return NULL;
return cpufreq_cpu_get(first_cpu);
}
/**
* rockchip_cpufreq_adjust_target() - Adjust cpu target frequency
* @cpu: CPU number
* @freq: Expected target frequency
*
* This adjusts cpu target frequency for reducing power consumption.
* Only one cluster can eanble frequency limit, and the cluster's
* maximum frequency will be limited to its threshold frequency, if the
* other cluster's frequency is geater than or equal to its threshold
* frequency.
*/
unsigned int rockchip_cpufreq_adjust_target(int cpu, unsigned int freq)
{
struct cpufreq_policy *policy;
struct cluster_info *cluster, *temp;
cluster = rockchip_cluster_info_lookup(cpu);
if (!cluster || !cluster->threshold_freq)
goto adjust_out;
if (cluster->freq_limit) {
if (freq <= cluster->threshold_freq)
goto adjust_out;
list_for_each_entry(temp, &cluster_info_list, list_head) {
if (temp->freq_limit || temp == cluster ||
temp->offline)
continue;
policy = rockchip_get_policy(temp);
if (!policy)
continue;
if (temp->threshold_freq &&
temp->threshold_freq <= policy->cur) {
cpufreq_cpu_put(policy);
return cluster->threshold_freq;
}
cpufreq_cpu_put(policy);
}
} else {
if (freq < cluster->threshold_freq)
goto adjust_out;
list_for_each_entry(temp, &cluster_info_list, list_head) {
if (!temp->freq_limit || temp == cluster ||
temp->offline)
continue;
policy = rockchip_get_policy(temp);
if (!policy)
continue;
if (temp->threshold_freq &&
temp->threshold_freq < policy->cur)
cpufreq_driver_target(policy,
temp->threshold_freq,
CPUFREQ_RELATION_H);
cpufreq_cpu_put(policy);
}
}
adjust_out:
return freq;
}
EXPORT_SYMBOL_GPL(rockchip_cpufreq_adjust_target);
static int __init rockchip_cpufreq_driver_init(void)
{
struct cluster_info *cluster, *pos;
struct cpufreq_dt_platform_data pdata = {0};
int cpu, ret, i = 0;
for_each_possible_cpu(cpu) {
cluster = rockchip_cluster_info_lookup(cpu);
if (cluster)
continue;
cluster = kzalloc(sizeof(*cluster), GFP_KERNEL);
if (!cluster) {
ret = -ENOMEM;
goto release_cluster_info;
}
ret = rockchip_cpufreq_cluster_init(cpu, cluster);
if (ret) {
if (ret != -ENOENT) {
pr_err("Failed to initialize dvfs info cpu%d\n",
cpu);
goto release_cluster_info;
}
/*
* As the OPP document said, only one OPP binding
* should be used per device.
* And if there are multiple clusters on rockchip
* platforms, we should use operating-points-v2.
* So if don't support operating-points-v2, there must
* be only one cluster, the list shuold be null.
*/
list_for_each_entry(pos, &cluster_info_list, list_head)
i++;
if (i)
goto release_cluster_info;
list_add(&cluster->list_head, &cluster_info_list);
goto next;
}
list_add(&cluster->list_head, &cluster_info_list);
}
next:
pdata.have_governor_per_policy = true;
pdata.suspend = rockchip_cpufreq_suspend;
return PTR_ERR_OR_ZERO(platform_device_register_data(NULL, "cpufreq-dt",
-1, (void *)&pdata,
sizeof(struct cpufreq_dt_platform_data)));
release_cluster_info:
list_for_each_entry_safe(cluster, pos, &cluster_info_list, list_head) {
list_del(&cluster->list_head);
kfree(cluster);
}
return ret;
}
module_init(rockchip_cpufreq_driver_init);
MODULE_AUTHOR("Finley Xiao <finley.xiao@rock-chips.com>");
MODULE_DESCRIPTION("Rockchip cpufreq driver");
MODULE_LICENSE("GPL v2");