618 lines
15 KiB
C
Executable File
618 lines
15 KiB
C
Executable File
/*
|
|
* Copyright (c) 2016 Park Bumgyu, Samsung Electronics Co., Ltd <bumgyu.park@samsung.com>
|
|
*
|
|
* 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.
|
|
*
|
|
* Exynos ACME(A Cpufreq that Meets Every chipset) driver implementation
|
|
*/
|
|
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
|
|
|
#include <linux/init.h>
|
|
#include <linux/of.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/cpu.h>
|
|
#include <linux/cpumask.h>
|
|
#include <linux/cpufreq.h>
|
|
#include <linux/pm_opp.h>
|
|
#include <linux/ems_service.h>
|
|
|
|
#include <soc/samsung/exynos-cpuhp.h>
|
|
|
|
#include "exynos-acme.h"
|
|
|
|
struct exynos_ufc_req {
|
|
int last_min_input;
|
|
int last_min_wo_boost_input;
|
|
int last_max_input;
|
|
} ufc_req = {
|
|
.last_min_input = -1,
|
|
.last_min_wo_boost_input = -1,
|
|
.last_max_input = -1,
|
|
};
|
|
|
|
/*********************************************************************
|
|
* SYSFS INTERFACES *
|
|
*********************************************************************/
|
|
/*
|
|
* Log2 of the number of scale size. The frequencies are scaled up or
|
|
* down as the multiple of this number.
|
|
*/
|
|
#define SCALE_SIZE 2
|
|
|
|
static ssize_t show_cpufreq_table(struct kobject *kobj,
|
|
struct kobj_attribute *attr, char *buf)
|
|
{
|
|
struct list_head *domains = get_domain_list();
|
|
struct exynos_cpufreq_domain *domain;
|
|
ssize_t count = 0;
|
|
int i, scale = 0;
|
|
|
|
list_for_each_entry_reverse(domain, domains, list) {
|
|
for (i = 0; i < domain->table_size; i++) {
|
|
unsigned int freq = domain->freq_table[i].frequency;
|
|
|
|
if (freq == CPUFREQ_ENTRY_INVALID)
|
|
continue;
|
|
|
|
count += snprintf(&buf[count], 10, "%d ",
|
|
freq >> (scale * SCALE_SIZE));
|
|
}
|
|
|
|
scale++;
|
|
}
|
|
|
|
count += snprintf(&buf[count - 1], 2, "\n");
|
|
|
|
return count - 1;
|
|
}
|
|
|
|
static ssize_t show_cpufreq_min_limit(struct kobject *kobj,
|
|
struct kobj_attribute *attr, char *buf)
|
|
{
|
|
return snprintf(buf, PAGE_SIZE, "%d\n", ufc_req.last_min_input);
|
|
}
|
|
|
|
static struct kpp kpp_ta;
|
|
static struct kpp kpp_fg;
|
|
|
|
static ssize_t store_cpufreq_min_limit(struct kobject *kobj,
|
|
struct kobj_attribute *attr, const char *buf,
|
|
size_t count)
|
|
{
|
|
struct list_head *domains = get_domain_list();
|
|
struct exynos_cpufreq_domain *domain;
|
|
int input, scale = -1;
|
|
unsigned int freq;
|
|
unsigned int req_limit_freq;
|
|
bool set_max = false;
|
|
bool set_limit = false;
|
|
int index = 0;
|
|
struct cpumask mask;
|
|
|
|
if (!sscanf(buf, "%8d", &input))
|
|
return -EINVAL;
|
|
|
|
if (!domains) {
|
|
pr_err("failed to get domains!\n");
|
|
return -ENXIO;
|
|
}
|
|
|
|
ufc_req.last_min_input = input;
|
|
|
|
list_for_each_entry_reverse(domain, domains, list) {
|
|
struct exynos_ufc *ufc, *r_ufc;
|
|
struct cpufreq_policy *policy = NULL;
|
|
|
|
cpumask_and(&mask, &domain->cpus, cpu_online_mask);
|
|
if (!cpumask_weight(&mask))
|
|
continue;
|
|
|
|
policy = cpufreq_cpu_get_raw(cpumask_any(&mask));
|
|
if (!policy)
|
|
continue;
|
|
|
|
ufc = list_entry(&domain->ufc_list, struct exynos_ufc, list);
|
|
|
|
r_ufc = NULL;
|
|
|
|
list_for_each_entry(ufc, &domain->ufc_list, list) {
|
|
if (ufc->info.ctrl_type == PM_QOS_MIN_LIMIT) {
|
|
r_ufc = ufc;
|
|
break;
|
|
}
|
|
}
|
|
|
|
scale++;
|
|
|
|
if (set_limit) {
|
|
req_limit_freq = min(req_limit_freq, domain->max_freq);
|
|
pm_qos_update_request(&domain->user_min_qos_req, req_limit_freq);
|
|
set_limit = false;
|
|
continue;
|
|
}
|
|
|
|
if (set_max) {
|
|
unsigned int qos = domain->max_freq;
|
|
|
|
if (domain->user_default_qos)
|
|
qos = domain->user_default_qos;
|
|
|
|
pm_qos_update_request(&domain->user_min_qos_req, qos);
|
|
continue;
|
|
}
|
|
|
|
/* Clear all constraint by cpufreq_min_limit */
|
|
if (input < 0) {
|
|
pm_qos_update_request(&domain->user_min_qos_req, 0);
|
|
kpp_request(STUNE_TOPAPP, &kpp_ta, 0);
|
|
kpp_request(STUNE_FOREGROUND, &kpp_fg, 0);
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* User inputs scaled down frequency. To recover real
|
|
* frequency, scale up frequency as multiple of 4.
|
|
* ex) domain2 = freq
|
|
* domain1 = freq * 4
|
|
* domain0 = freq * 16
|
|
*/
|
|
freq = input << (scale * SCALE_SIZE);
|
|
|
|
if (freq < domain->min_freq) {
|
|
pm_qos_update_request(&domain->user_min_qos_req, 0);
|
|
continue;
|
|
}
|
|
|
|
if (r_ufc) {
|
|
if (policy->freq_table == NULL)
|
|
continue;
|
|
index = cpufreq_frequency_table_target(policy, freq, CPUFREQ_RELATION_L);
|
|
req_limit_freq = r_ufc->info.freq_table[index].limit_freq;
|
|
if (req_limit_freq)
|
|
set_limit = true;
|
|
}
|
|
|
|
freq = min(freq, domain->max_freq);
|
|
pm_qos_update_request(&domain->user_min_qos_req, freq);
|
|
|
|
kpp_request(STUNE_TOPAPP, &kpp_ta, domain->user_boost);
|
|
kpp_request(STUNE_FOREGROUND, &kpp_fg, domain->user_boost);
|
|
|
|
set_max = true;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static ssize_t show_cpufreq_min_limit_wo_boost(struct kobject *kobj,
|
|
struct kobj_attribute *attr, char *buf)
|
|
{
|
|
return snprintf(buf, PAGE_SIZE, "%d\n", ufc_req.last_min_wo_boost_input);
|
|
}
|
|
|
|
static ssize_t store_cpufreq_min_limit_wo_boost(struct kobject *kobj,
|
|
struct kobj_attribute *attr, const char *buf,
|
|
size_t count)
|
|
{
|
|
struct list_head *domains = get_domain_list();
|
|
struct exynos_cpufreq_domain *domain;
|
|
int input, scale = -1;
|
|
unsigned int freq;
|
|
unsigned int req_limit_freq;
|
|
bool set_max = false;
|
|
bool set_limit = false;
|
|
int index = 0;
|
|
struct cpumask mask;
|
|
|
|
if (!sscanf(buf, "%8d", &input))
|
|
return -EINVAL;
|
|
|
|
if (!domains) {
|
|
pr_err("failed to get domains!\n");
|
|
return -ENXIO;
|
|
}
|
|
|
|
ufc_req.last_min_wo_boost_input = input;
|
|
|
|
list_for_each_entry_reverse(domain, domains, list) {
|
|
struct exynos_ufc *ufc, *r_ufc;
|
|
struct cpufreq_policy *policy = NULL;
|
|
|
|
cpumask_and(&mask, &domain->cpus, cpu_online_mask);
|
|
if (!cpumask_weight(&mask))
|
|
continue;
|
|
|
|
policy = cpufreq_cpu_get_raw(cpumask_any(&mask));
|
|
if (!policy)
|
|
continue;
|
|
|
|
ufc = list_entry(&domain->ufc_list, struct exynos_ufc, list);
|
|
|
|
r_ufc = NULL;
|
|
|
|
list_for_each_entry(ufc, &domain->ufc_list, list) {
|
|
if (ufc->info.ctrl_type == PM_QOS_MIN_WO_BOOST_LIMIT) {
|
|
r_ufc = ufc;
|
|
break;
|
|
}
|
|
}
|
|
|
|
scale++;
|
|
|
|
if (set_limit) {
|
|
req_limit_freq = min(req_limit_freq, domain->max_freq);
|
|
pm_qos_update_request(&domain->user_min_qos_req, req_limit_freq);
|
|
set_limit = false;
|
|
continue;
|
|
}
|
|
|
|
if (set_max) {
|
|
unsigned int qos = domain->max_freq;
|
|
|
|
if (domain->user_default_qos)
|
|
qos = domain->user_default_qos;
|
|
|
|
pm_qos_update_request(&domain->user_min_qos_wo_boost_req, qos);
|
|
continue;
|
|
}
|
|
|
|
/* Clear all constraint by cpufreq_min_limit */
|
|
if (input < 0) {
|
|
pm_qos_update_request(&domain->user_min_qos_wo_boost_req, 0);
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* User inputs scaled down frequency. To recover real
|
|
* frequency, scale up frequency as multiple of 4.
|
|
* ex) domain2 = freq
|
|
* domain1 = freq * 4
|
|
* domain0 = freq * 16
|
|
*/
|
|
freq = input << (scale * SCALE_SIZE);
|
|
|
|
if (freq < domain->min_freq) {
|
|
pm_qos_update_request(&domain->user_min_qos_wo_boost_req, 0);
|
|
continue;
|
|
}
|
|
|
|
if (r_ufc) {
|
|
if (policy->freq_table == NULL)
|
|
continue;
|
|
index = cpufreq_frequency_table_target(policy, freq, CPUFREQ_RELATION_L);
|
|
req_limit_freq = r_ufc->info.freq_table[index].limit_freq;
|
|
if (req_limit_freq)
|
|
set_limit = true;
|
|
}
|
|
|
|
freq = min(freq, domain->max_freq);
|
|
pm_qos_update_request(&domain->user_min_qos_wo_boost_req, freq);
|
|
|
|
set_max = true;
|
|
}
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
static ssize_t show_cpufreq_max_limit(struct kobject *kobj,
|
|
struct kobj_attribute *attr, char *buf)
|
|
{
|
|
return snprintf(buf, PAGE_SIZE, "%d\n", ufc_req.last_max_input);
|
|
}
|
|
|
|
static void enable_domain_cpus(struct exynos_cpufreq_domain *domain)
|
|
{
|
|
struct cpumask mask;
|
|
|
|
if (domain == first_domain())
|
|
return;
|
|
|
|
cpumask_or(&mask, cpu_online_mask, &domain->cpus);
|
|
exynos_cpuhp_request("ACME", mask, 0);
|
|
}
|
|
|
|
static void disable_domain_cpus(struct exynos_cpufreq_domain *domain)
|
|
{
|
|
struct cpumask mask;
|
|
|
|
if (domain == first_domain())
|
|
return;
|
|
|
|
cpumask_andnot(&mask, cpu_online_mask, &domain->cpus);
|
|
exynos_cpuhp_request("ACME", mask, 0);
|
|
}
|
|
|
|
static ssize_t store_cpufreq_max_limit(struct kobject *kobj, struct kobj_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
struct list_head *domains = get_domain_list();
|
|
struct exynos_cpufreq_domain *domain;
|
|
int input, scale = -1;
|
|
unsigned int freq;
|
|
bool set_max = false;
|
|
unsigned int req_limit_freq;
|
|
bool set_limit = false;
|
|
int index = 0;
|
|
struct cpumask mask;
|
|
|
|
if (!sscanf(buf, "%8d", &input))
|
|
return -EINVAL;
|
|
|
|
ufc_req.last_max_input = input;
|
|
|
|
list_for_each_entry_reverse(domain, domains, list) {
|
|
struct exynos_ufc *ufc, *r_ufc;
|
|
struct cpufreq_policy *policy = NULL;
|
|
|
|
cpumask_and(&mask, &domain->cpus, cpu_online_mask);
|
|
if (cpumask_weight(&mask))
|
|
policy = cpufreq_cpu_get_raw(cpumask_any(&mask));
|
|
|
|
ufc = list_entry(&domain->ufc_list, struct exynos_ufc, list);
|
|
|
|
r_ufc = NULL;
|
|
|
|
list_for_each_entry(ufc, &domain->ufc_list, list) {
|
|
if (ufc->info.ctrl_type == PM_QOS_MAX_LIMIT) {
|
|
r_ufc = ufc;
|
|
break;
|
|
}
|
|
}
|
|
|
|
scale++;
|
|
|
|
if (set_limit) {
|
|
req_limit_freq = max(req_limit_freq, domain->min_freq);
|
|
pm_qos_update_request(&domain->user_max_qos_req,
|
|
req_limit_freq);
|
|
set_limit = false;
|
|
continue;
|
|
}
|
|
|
|
if (set_max) {
|
|
pm_qos_update_request(&domain->user_max_qos_req,
|
|
domain->max_freq);
|
|
continue;
|
|
}
|
|
|
|
/* Clear all constraint by cpufreq_max_limit */
|
|
if (input < 0) {
|
|
enable_domain_cpus(domain);
|
|
pm_qos_update_request(&domain->user_max_qos_req,
|
|
domain->max_freq);
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* User inputs scaled down frequency. To recover real
|
|
* frequency, scale up frequency as multiple of 4.
|
|
* ex) domain2 = freq
|
|
* domain1 = freq * 4
|
|
* domain0 = freq * 16
|
|
*/
|
|
freq = input << (scale * SCALE_SIZE);
|
|
|
|
if (policy && r_ufc) {
|
|
if (policy->freq_table == NULL)
|
|
continue;
|
|
index = cpufreq_frequency_table_target(policy, freq, CPUFREQ_RELATION_L);
|
|
req_limit_freq = r_ufc->info.freq_table[index].limit_freq;
|
|
if (req_limit_freq)
|
|
set_limit = true;
|
|
}
|
|
|
|
if (freq < domain->min_freq) {
|
|
set_limit = false;
|
|
pm_qos_update_request(&domain->user_max_qos_req, 0);
|
|
disable_domain_cpus(domain);
|
|
continue;
|
|
}
|
|
|
|
enable_domain_cpus(domain);
|
|
|
|
freq = max(freq, domain->min_freq);
|
|
pm_qos_update_request(&domain->user_max_qos_req, freq);
|
|
|
|
set_max = true;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static struct kobj_attribute cpufreq_table =
|
|
__ATTR(cpufreq_table, 0444 , show_cpufreq_table, NULL);
|
|
static struct kobj_attribute cpufreq_min_limit =
|
|
__ATTR(cpufreq_min_limit, 0644,
|
|
show_cpufreq_min_limit, store_cpufreq_min_limit);
|
|
static struct kobj_attribute cpufreq_min_limit_wo_boost =
|
|
__ATTR(cpufreq_min_limit_wo_boost, 0644,
|
|
show_cpufreq_min_limit_wo_boost, store_cpufreq_min_limit_wo_boost);
|
|
static struct kobj_attribute cpufreq_max_limit =
|
|
__ATTR(cpufreq_max_limit, 0644,
|
|
show_cpufreq_max_limit, store_cpufreq_max_limit);
|
|
|
|
static __init void init_sysfs(void)
|
|
{
|
|
if (sysfs_create_file(power_kobj, &cpufreq_table.attr))
|
|
pr_err("failed to create cpufreq_table node\n");
|
|
|
|
if (sysfs_create_file(power_kobj, &cpufreq_min_limit.attr))
|
|
pr_err("failed to create cpufreq_min_limit node\n");
|
|
|
|
if (sysfs_create_file(power_kobj, &cpufreq_min_limit_wo_boost.attr))
|
|
pr_err("failed to create cpufreq_min_limit_wo_boost node\n");
|
|
|
|
if (sysfs_create_file(power_kobj, &cpufreq_max_limit.attr))
|
|
pr_err("failed to create cpufreq_max_limit node\n");
|
|
|
|
}
|
|
|
|
static int parse_ufc_ctrl_info(struct exynos_cpufreq_domain *domain,
|
|
struct device_node *dn)
|
|
{
|
|
unsigned int val;
|
|
|
|
if (!of_property_read_u32(dn, "user-default-qos", &val))
|
|
domain->user_default_qos = val;
|
|
|
|
if (!of_property_read_u32(dn, "user-boost", &val))
|
|
domain->user_boost = val;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static __init void init_pm_qos(struct exynos_cpufreq_domain *domain)
|
|
{
|
|
pm_qos_add_request(&domain->user_min_qos_req,
|
|
domain->pm_qos_min_class, domain->min_freq);
|
|
pm_qos_add_request(&domain->user_max_qos_req,
|
|
domain->pm_qos_max_class, domain->max_freq);
|
|
pm_qos_add_request(&domain->user_min_qos_wo_boost_req,
|
|
domain->pm_qos_min_class, domain->min_freq);
|
|
}
|
|
|
|
int ufc_domain_init(struct exynos_cpufreq_domain *domain)
|
|
{
|
|
struct device_node *dn, *child;
|
|
struct cpumask mask;
|
|
const char *buf;
|
|
|
|
dn = of_find_node_by_name(NULL, "cpufreq-ufc");
|
|
|
|
while((dn = of_find_node_by_type(dn, "cpufreq-userctrl"))) {
|
|
of_property_read_string(dn, "shared-cpus", &buf);
|
|
cpulist_parse(buf, &mask);
|
|
if (cpumask_intersects(&mask, &domain->cpus)) {
|
|
printk("found!\n");
|
|
break;
|
|
}
|
|
}
|
|
|
|
for_each_child_of_node(dn, child) {
|
|
struct exynos_ufc *ufc;
|
|
|
|
ufc = kzalloc(sizeof(struct exynos_ufc), GFP_KERNEL);
|
|
if (!ufc)
|
|
return -ENOMEM;
|
|
|
|
ufc->info.freq_table = kzalloc(sizeof(struct exynos_ufc_freq)
|
|
* domain->table_size, GFP_KERNEL);
|
|
|
|
if (!ufc->info.freq_table) {
|
|
kfree(ufc);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
list_add_tail(&ufc->list, &domain->ufc_list);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int __init init_ufc_table_dt(struct exynos_cpufreq_domain *domain,
|
|
struct device_node *dn)
|
|
{
|
|
struct device_node *child;
|
|
struct exynos_ufc_freq *table;
|
|
struct exynos_ufc *ufc;
|
|
int size, index, c_index;
|
|
int ret;
|
|
|
|
ufc = list_entry(&domain->ufc_list, struct exynos_ufc, list);
|
|
|
|
pr_info("Initialize ufc table for Domain %d\n",domain->id);
|
|
|
|
for_each_child_of_node(dn, child) {
|
|
|
|
ufc = list_next_entry(ufc, list);
|
|
|
|
if (of_property_read_u32(child, "ctrl-type", &ufc->info.ctrl_type))
|
|
continue;
|
|
|
|
size = of_property_count_u32_elems(child, "table");
|
|
if (size < 0)
|
|
return size;
|
|
|
|
table = kzalloc(sizeof(struct exynos_ufc_freq) * size / 2, GFP_KERNEL);
|
|
if (!table)
|
|
return -ENOMEM;
|
|
|
|
ret = of_property_read_u32_array(child, "table", (unsigned int *)table, size);
|
|
if (ret)
|
|
return -EINVAL;
|
|
|
|
pr_info("Register UFC Type-%d for Domain %d \n",ufc->info.ctrl_type, domain->id);
|
|
for (index = 0; index < domain->table_size; index++) {
|
|
unsigned int freq = domain->freq_table[index].frequency;
|
|
|
|
if (freq == CPUFREQ_ENTRY_INVALID)
|
|
continue;
|
|
|
|
for (c_index = 0; c_index < size / 2; c_index++) {
|
|
if (freq <= table[c_index].master_freq) {
|
|
ufc->info.freq_table[index].limit_freq = table[c_index].limit_freq;
|
|
}
|
|
if (freq >= table[c_index].master_freq)
|
|
break;
|
|
}
|
|
pr_info("Master_freq : %u kHz - limit_freq : %u kHz\n",
|
|
ufc->info.freq_table[index].master_freq,
|
|
ufc->info.freq_table[index].limit_freq);
|
|
}
|
|
kfree(table);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int __init exynos_ufc_init(void)
|
|
{
|
|
struct device_node *dn = NULL;
|
|
const char *buf;
|
|
struct exynos_cpufreq_domain *domain;
|
|
int ret = 0;
|
|
|
|
while((dn = of_find_node_by_type(dn, "cpufreq-userctrl"))) {
|
|
struct cpumask shared_mask;
|
|
|
|
ret = of_property_read_string(dn, "shared-cpus", &buf);
|
|
if (ret) {
|
|
pr_err("failed to get shared-cpus for ufc\n");
|
|
goto exit;
|
|
}
|
|
|
|
cpulist_parse(buf, &shared_mask);
|
|
domain = find_domain_cpumask(&shared_mask);
|
|
if (!domain)
|
|
pr_err("Can't found domain for ufc!\n");
|
|
|
|
/* Initialize user control information from dt */
|
|
ret = parse_ufc_ctrl_info(domain, dn);
|
|
if (ret) {
|
|
pr_err("failed to get ufc ctrl info\n");
|
|
goto exit;
|
|
}
|
|
|
|
/* Parse user frequency ctrl table info from dt */
|
|
ret = init_ufc_table_dt(domain, dn);
|
|
if (ret) {
|
|
pr_err("failed to parse frequency table for ufc ctrl\n");
|
|
goto exit;
|
|
}
|
|
/* Initialize PM QoS */
|
|
init_pm_qos(domain);
|
|
pr_info("Complete to initialize domain%d\n",domain->id);
|
|
}
|
|
|
|
init_sysfs();
|
|
|
|
pr_info("Initialized Exynos UFC(User-Frequency-Ctrl) driver\n");
|
|
exit:
|
|
return 0;
|
|
}
|
|
late_initcall(exynos_ufc_init);
|