2017 lines
53 KiB
C
Executable file
2017 lines
53 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/tick.h>
|
|
#include <linux/pm_opp.h>
|
|
#include <soc/samsung/cpu_cooling.h>
|
|
#include <linux/suspend.h>
|
|
#include <linux/platform_device.h>
|
|
#include <uapi/linux/sched/types.h>
|
|
#include <linux/ems.h>
|
|
|
|
#include <soc/samsung/debug-snapshot.h>
|
|
#include <soc/samsung/cal-if.h>
|
|
#include <soc/samsung/ect_parser.h>
|
|
#include <soc/samsung/freq-qos-tracer.h>
|
|
#include <soc/samsung/exynos-acme.h>
|
|
#include <soc/samsung/exynos-dm.h>
|
|
|
|
#define CREATE_TRACE_POINTS
|
|
#include <trace/events/acme.h>
|
|
|
|
#include "exynos-acme.h"
|
|
|
|
#if defined(CONFIG_ARM_EXYNOS_CLDVFS_SYSFS)
|
|
#include <asm/io.h>
|
|
#endif
|
|
|
|
#if defined(CONFIG_SEC_FACTORY)
|
|
#if defined(CONFIG_SEC_FACTORY_INTERPOSER)
|
|
#define BOOTING_BOOST_TIME 150000
|
|
#else
|
|
#define BOOTING_BOOST_TIME 60000
|
|
#endif
|
|
#else // !defined(CONFIG_SEC_FACTORY)
|
|
#define BOOTING_BOOST_TIME 40000
|
|
#endif
|
|
|
|
#if defined(CONFIG_SEC_FACTORY)
|
|
enum {
|
|
FLEXBOOT_LIT = 1, // LIT Core Flex and All Level Freq Change Allow
|
|
FLEXBOOT_MID, // MID Core Flex and All Level Freq Change Allow
|
|
FLEXBOOT_BIG, // BIG Core Flex and All Level Freq Change Allow
|
|
FLEXBOOT_FLEX_ONLY, // All Core Flex only. max limit
|
|
FLEXBOOT_ALL, // All Core Flex and All Level Freq Change Allow
|
|
};
|
|
|
|
int flexable_cpu_boot;
|
|
|
|
module_param(flexable_cpu_boot, int, 0440);
|
|
EXPORT_SYMBOL(flexable_cpu_boot);
|
|
#endif
|
|
/*
|
|
* list head of cpufreq domain
|
|
*/
|
|
static LIST_HEAD(domains);
|
|
|
|
/*
|
|
* transition notifier if fast switch is enabled.
|
|
*/
|
|
static DEFINE_RWLOCK(exynos_cpufreq_transition_notifier_lock);
|
|
static RAW_NOTIFIER_HEAD(exynos_cpufreq_transition_notifier_list);
|
|
static void exynos_cpufreq_notify_transition(struct cpufreq_policy *policy,
|
|
struct cpufreq_freqs *freqs,
|
|
unsigned int state,
|
|
int retval);
|
|
|
|
/*********************************************************************
|
|
* HELPER FUNCTION *
|
|
*********************************************************************/
|
|
static struct exynos_cpufreq_domain *find_domain(unsigned int cpu)
|
|
{
|
|
struct exynos_cpufreq_domain *domain;
|
|
|
|
list_for_each_entry(domain, &domains, list)
|
|
if (cpumask_test_cpu(cpu, &domain->cpus))
|
|
return domain;
|
|
|
|
pr_err("cannot find cpufreq domain by cpu\n");
|
|
return NULL;
|
|
}
|
|
|
|
static void enable_domain(struct exynos_cpufreq_domain *domain)
|
|
{
|
|
mutex_lock(&domain->lock);
|
|
domain->enabled = true;
|
|
mutex_unlock(&domain->lock);
|
|
}
|
|
|
|
static void disable_domain(struct exynos_cpufreq_domain *domain)
|
|
{
|
|
mutex_lock(&domain->lock);
|
|
domain->enabled = false;
|
|
mutex_unlock(&domain->lock);
|
|
}
|
|
|
|
static unsigned int resolve_freq_wo_clamp(struct cpufreq_policy *policy,
|
|
unsigned int target_freq, int flag)
|
|
{
|
|
unsigned int index = -1;
|
|
|
|
if (flag == CPUFREQ_RELATION_L)
|
|
index = cpufreq_table_find_index_al(policy, target_freq);
|
|
else if (flag == CPUFREQ_RELATION_H)
|
|
index = cpufreq_table_find_index_ah(policy, target_freq);
|
|
|
|
if (index < 0) {
|
|
pr_err("target frequency(%d) out of range\n", target_freq);
|
|
return 0;
|
|
}
|
|
|
|
return policy->freq_table[index].frequency;
|
|
}
|
|
|
|
/* Find lowest freq at or above target in a table in ascending order */
|
|
static inline int exynos_cpufreq_find_index(struct exynos_cpufreq_domain *domain,
|
|
unsigned int target_freq)
|
|
{
|
|
struct cpufreq_frequency_table *table = domain->freq_table;
|
|
struct cpufreq_frequency_table *pos;
|
|
unsigned int freq;
|
|
int idx, best = -1;
|
|
|
|
cpufreq_for_each_valid_entry_idx(pos, table, idx) {
|
|
freq = pos->frequency;
|
|
|
|
if (freq >= target_freq)
|
|
return idx;
|
|
|
|
best = idx;
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
/*********************************************************************
|
|
* PRE/POST HANDLING FOR SCALING *
|
|
*********************************************************************/
|
|
static int pre_scale(struct cpufreq_freqs *freqs)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static int post_scale(struct cpufreq_freqs *freqs)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
/*********************************************************************
|
|
* FREQUENCY SCALING *
|
|
*********************************************************************/
|
|
static unsigned int get_freq(struct exynos_cpufreq_domain *domain)
|
|
{
|
|
unsigned int freq;
|
|
|
|
/* valid_freq_flag indicates whether boot freq is in the freq table or not.
|
|
* so, to prevent cpufreq mulfuntion, return old freq instead of cal freq
|
|
* until the frequency changes even once.
|
|
*/
|
|
if (unlikely(!domain->valid_freq_flag && domain->old))
|
|
return domain->old;
|
|
|
|
freq = (unsigned int)cal_dfs_get_rate(domain->cal_id);
|
|
if (!freq) {
|
|
/* On changing state, CAL returns 0 */
|
|
freq = domain->old;
|
|
}
|
|
|
|
return freq;
|
|
}
|
|
|
|
static int set_freq(struct exynos_cpufreq_domain *domain,
|
|
unsigned int target_freq)
|
|
{
|
|
int err;
|
|
|
|
dbg_snapshot_printk("ID %d: %d -> %d (%d)\n",
|
|
domain->id, domain->old, target_freq, DSS_FLAG_IN);
|
|
|
|
if (domain->fast_switch_possible && domain->dvfs_mode == NON_BLOCKING) {
|
|
err = cal_dfs_set_rate_fast(domain->cal_id, target_freq);
|
|
}
|
|
else
|
|
err = cal_dfs_set_rate(domain->cal_id, target_freq);
|
|
|
|
if (err < 0)
|
|
pr_err("failed to scale frequency of domain%d (%d -> %d)\n",
|
|
domain->id, domain->old, target_freq);
|
|
|
|
if (!domain->fast_switch_possible || domain->dvfs_mode == BLOCKING)
|
|
trace_acme_scale_freq(domain->id, domain->old, target_freq, "end", 0);
|
|
|
|
dbg_snapshot_printk("ID %d: %d -> %d (%d)\n",
|
|
domain->id, domain->old, target_freq, DSS_FLAG_OUT);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int scale(struct exynos_cpufreq_domain *domain,
|
|
struct cpufreq_policy *policy,
|
|
unsigned int target_freq)
|
|
{
|
|
int ret;
|
|
struct cpufreq_freqs freqs = {
|
|
.policy = policy,
|
|
.old = domain->old,
|
|
.new = target_freq,
|
|
.flags = 0,
|
|
};
|
|
|
|
exynos_cpufreq_notify_transition(policy, &freqs, CPUFREQ_PRECHANGE, 0);
|
|
|
|
dbg_snapshot_freq(domain->dss_type, domain->old, target_freq, DSS_FLAG_IN);
|
|
|
|
ret = pre_scale(&freqs);
|
|
if (ret)
|
|
goto fail_scale;
|
|
|
|
/* Scale frequency by hooked function, set_freq() */
|
|
ret = set_freq(domain, target_freq);
|
|
if (ret)
|
|
goto fail_scale;
|
|
|
|
ret = post_scale(&freqs);
|
|
if (ret)
|
|
goto fail_scale;
|
|
|
|
fail_scale:
|
|
/* In scaling failure case, logs -1 to exynos snapshot */
|
|
dbg_snapshot_freq(domain->dss_type, domain->old, target_freq,
|
|
ret < 0 ? ret : DSS_FLAG_OUT);
|
|
|
|
/* if error occur during frequency scaling, do not set valid_freq_flag */
|
|
if (unlikely(!(ret || domain->valid_freq_flag)))
|
|
domain->valid_freq_flag = true;
|
|
|
|
exynos_cpufreq_notify_transition(policy, &freqs, CPUFREQ_POSTCHANGE, ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*********************************************************************
|
|
* EXYNOS CPUFREQ DRIVER INTERFACE *
|
|
*********************************************************************/
|
|
static int exynos_cpufreq_init(struct cpufreq_policy *policy)
|
|
{
|
|
struct exynos_cpufreq_domain *domain = find_domain(policy->cpu);
|
|
|
|
if (!domain)
|
|
return -EINVAL;
|
|
|
|
policy->fast_switch_possible = domain->fast_switch_possible;
|
|
policy->freq_table = domain->freq_table;
|
|
policy->cpuinfo.transition_latency = TRANSITION_LATENCY;
|
|
policy->dvfs_possible_from_any_cpu = true;
|
|
cpumask_copy(policy->cpus, &domain->cpus);
|
|
|
|
pr_info("Initialize cpufreq policy%d\n", policy->cpu);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static unsigned int exynos_cpufreq_fast_switch(struct cpufreq_policy *policy,
|
|
unsigned int target_freq)
|
|
{
|
|
struct exynos_cpufreq_domain *domain;
|
|
unsigned long fast_switch_freq = (unsigned long)target_freq;
|
|
unsigned long flags;
|
|
|
|
domain = find_domain(policy->cpu);
|
|
if (!domain)
|
|
return 0;
|
|
|
|
raw_spin_lock_irqsave(&domain->fast_switch_update_lock, flags);
|
|
if (domain->dvfs_mode == NON_BLOCKING) {
|
|
int ret;
|
|
unsigned long old = domain->old;
|
|
|
|
trace_acme_scale_freq(domain->id, old, fast_switch_freq, "start", 0);
|
|
ret = DM_CALL(domain->dm_type, &fast_switch_freq);
|
|
if (ret) {
|
|
trace_acme_scale_freq(domain->id, old, fast_switch_freq, "fail", 0);
|
|
fast_switch_freq = 0;
|
|
}
|
|
} else {
|
|
if (!domain->work_in_progress) {
|
|
domain->work_in_progress = true;
|
|
domain->cached_fast_switch_freq = fast_switch_freq;
|
|
trace_acme_scale_freq(domain->id, domain->old, fast_switch_freq, "start", 0);
|
|
irq_work_queue(&domain->fast_switch_irq_work);
|
|
} else
|
|
fast_switch_freq = 0;
|
|
}
|
|
raw_spin_unlock_irqrestore(&domain->fast_switch_update_lock, flags);
|
|
|
|
return fast_switch_freq;
|
|
}
|
|
|
|
static unsigned int exynos_cpufreq_resolve_freq(struct cpufreq_policy *policy,
|
|
unsigned int target_freq)
|
|
{
|
|
unsigned int index;
|
|
|
|
index = cpufreq_frequency_table_target(policy, target_freq, CPUFREQ_RELATION_L);
|
|
if (index < 0) {
|
|
pr_err("target frequency(%d) out of range\n", target_freq);
|
|
return 0;
|
|
}
|
|
|
|
return policy->freq_table[index].frequency;
|
|
}
|
|
|
|
static int exynos_cpufreq_verify(struct cpufreq_policy_data *new_policy)
|
|
{
|
|
int policy_cpu = new_policy->cpu;
|
|
struct exynos_cpufreq_domain *domain;
|
|
unsigned long max_capacity, capacity;
|
|
struct cpufreq_policy *policy;
|
|
unsigned int min = new_policy->min, max = new_policy->max;
|
|
|
|
domain = find_domain(policy_cpu);
|
|
if (!domain)
|
|
return -EINVAL;
|
|
|
|
policy = cpufreq_cpu_get(policy_cpu);
|
|
if (!policy)
|
|
goto verify_freq_range;
|
|
|
|
/* if minimum frequency is updated, find validate frequency from the table */
|
|
if (min != policy->min)
|
|
min = resolve_freq_wo_clamp(policy, min, CPUFREQ_RELATION_L);
|
|
|
|
/* if maximum frequency is updated, find validate frequency from the table */
|
|
if (max != policy->max)
|
|
max = resolve_freq_wo_clamp(policy, max, CPUFREQ_RELATION_H);
|
|
|
|
/*
|
|
* if corrected the minimum frequency is higher than the maximum frequency,
|
|
* replace it maximum. we don't want that the minimum overs the maximum anytime
|
|
*/
|
|
if (min > max)
|
|
min = max;
|
|
|
|
new_policy->min = min;
|
|
new_policy->max = max;
|
|
|
|
cpufreq_cpu_put(policy);
|
|
|
|
verify_freq_range:
|
|
/* clamp with cpuinfo.max/min and check whether valid frequency exist or not */
|
|
cpufreq_frequency_table_verify(new_policy, new_policy->freq_table);
|
|
|
|
policy_update_call_to_DM(domain->dm_type,
|
|
new_policy->min, new_policy->max);
|
|
|
|
max_capacity = arch_scale_cpu_capacity(policy_cpu);
|
|
capacity = new_policy->max * max_capacity
|
|
/ new_policy->cpuinfo.max_freq;
|
|
arch_set_thermal_pressure(&domain->cpus, max_capacity - capacity);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int __exynos_cpufreq_target(struct cpufreq_policy *policy,
|
|
unsigned int target_freq,
|
|
unsigned int relation)
|
|
{
|
|
struct exynos_cpufreq_domain *domain = find_domain(policy->cpu);
|
|
int ret = 0;
|
|
|
|
if (!domain)
|
|
return -EINVAL;
|
|
|
|
if (!domain->fast_switch_possible || domain->dvfs_mode == BLOCKING)
|
|
mutex_lock(&domain->lock);
|
|
|
|
if (!domain->enabled) {
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
target_freq = cpufreq_driver_resolve_freq(policy, target_freq);
|
|
|
|
/* Target is same as current, skip scaling */
|
|
if (domain->old == target_freq) {
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
#define TEN_MHZ (10000)
|
|
if (!domain->fast_switch_possible
|
|
&& abs(domain->old - get_freq(domain)) > TEN_MHZ) {
|
|
pr_err("oops, inconsistency between domain->old:%d, real clk:%d\n",
|
|
domain->old, get_freq(domain));
|
|
// BUG_ON(1);
|
|
}
|
|
#undef TEN_MHZ
|
|
|
|
ret = scale(domain, policy, target_freq);
|
|
if (ret)
|
|
goto out;
|
|
|
|
pr_debug("CPUFREQ domain%d frequency change %u kHz -> %u kHz\n",
|
|
domain->id, domain->old, target_freq);
|
|
|
|
domain->old = target_freq;
|
|
|
|
out:
|
|
if (!domain->fast_switch_possible || domain->dvfs_mode == BLOCKING)
|
|
mutex_unlock(&domain->lock);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int exynos_cpufreq_target(struct cpufreq_policy *policy,
|
|
unsigned int target_freq,
|
|
unsigned int relation)
|
|
{
|
|
struct exynos_cpufreq_domain *domain = find_domain(policy->cpu);
|
|
unsigned long freq;
|
|
|
|
if (!domain)
|
|
return -EINVAL;
|
|
|
|
trace_acme_scale_freq(domain->id, domain->old, target_freq, "start", 0);
|
|
|
|
if (list_empty(&domain->dm_list))
|
|
return __exynos_cpufreq_target(policy, target_freq, relation);
|
|
|
|
freq = (unsigned long)target_freq;
|
|
|
|
return DM_CALL(domain->dm_type, &freq);
|
|
}
|
|
|
|
static unsigned int exynos_cpufreq_get(unsigned int cpu)
|
|
{
|
|
struct exynos_cpufreq_domain *domain = find_domain(cpu);
|
|
|
|
if (!domain)
|
|
return 0;
|
|
|
|
return get_freq(domain);
|
|
}
|
|
|
|
static int __exynos_cpufreq_suspend(struct cpufreq_policy *policy,
|
|
struct exynos_cpufreq_domain *domain)
|
|
{
|
|
unsigned int freq;
|
|
struct work_struct *update_work = &policy->update;
|
|
|
|
if (!domain)
|
|
return 0;
|
|
|
|
mutex_lock(&domain->lock);
|
|
mutex_unlock(&domain->lock);
|
|
|
|
freq = domain->resume_freq;
|
|
|
|
freq_qos_update_request(policy->min_freq_req, freq);
|
|
freq_qos_update_request(policy->max_freq_req, freq);
|
|
|
|
flush_work(update_work);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int exynos_cpufreq_suspend(struct cpufreq_policy *policy)
|
|
{
|
|
return __exynos_cpufreq_suspend(policy, find_domain(policy->cpu));
|
|
}
|
|
|
|
static int __exynos_cpufreq_resume(struct cpufreq_policy *policy,
|
|
struct exynos_cpufreq_domain *domain)
|
|
{
|
|
if (!domain)
|
|
return -EINVAL;
|
|
|
|
mutex_lock(&domain->lock);
|
|
mutex_unlock(&domain->lock);
|
|
|
|
freq_qos_update_request(policy->max_freq_req, domain->max_freq);
|
|
freq_qos_update_request(policy->min_freq_req, domain->min_freq);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int exynos_cpufreq_resume(struct cpufreq_policy *policy)
|
|
{
|
|
return __exynos_cpufreq_resume(policy, find_domain(policy->cpu));
|
|
}
|
|
|
|
static int exynos_cpufreq_update_limit(struct cpufreq_policy *policy)
|
|
{
|
|
bool own;
|
|
unsigned long owner = atomic_long_read(&policy->rwsem.owner);
|
|
|
|
owner = owner & ~0x7;
|
|
own = (struct task_struct *)owner == current;
|
|
|
|
if (own)
|
|
refresh_frequency_limits(policy);
|
|
else {
|
|
down_write(&policy->rwsem);
|
|
refresh_frequency_limits(policy);
|
|
up_write(&policy->rwsem);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int exynos_cpufreq_notifier_min(struct notifier_block *nb,
|
|
unsigned long freq,
|
|
void *data)
|
|
{
|
|
struct cpufreq_policy *policy =
|
|
container_of(nb, struct cpufreq_policy, nb_min);
|
|
|
|
return exynos_cpufreq_update_limit(policy);
|
|
}
|
|
|
|
static int exynos_cpufreq_notifier_max(struct notifier_block *nb,
|
|
unsigned long freq,
|
|
void *data)
|
|
{
|
|
struct cpufreq_policy *policy =
|
|
container_of(nb, struct cpufreq_policy, nb_max);
|
|
|
|
return exynos_cpufreq_update_limit(policy);
|
|
}
|
|
|
|
static void exynos_cpufreq_ready(struct cpufreq_policy *policy)
|
|
{
|
|
down_write(&policy->rwsem);
|
|
policy->nb_min.notifier_call = exynos_cpufreq_notifier_min;
|
|
policy->nb_max.notifier_call = exynos_cpufreq_notifier_max;
|
|
up_write(&policy->rwsem);
|
|
}
|
|
|
|
static int exynos_cpufreq_pm_notifier(struct notifier_block *notifier,
|
|
unsigned long pm_event, void *v)
|
|
{
|
|
struct exynos_cpufreq_domain *domain;
|
|
struct cpufreq_policy *policy;
|
|
|
|
switch (pm_event) {
|
|
case PM_SUSPEND_PREPARE:
|
|
list_for_each_entry_reverse(domain, &domains, list) {
|
|
policy = cpufreq_cpu_get(cpumask_any(&domain->cpus));
|
|
if (!policy)
|
|
continue;
|
|
if (__exynos_cpufreq_suspend(policy, domain)) {
|
|
cpufreq_cpu_put(policy);
|
|
return NOTIFY_BAD;
|
|
}
|
|
cpufreq_cpu_put(policy);
|
|
}
|
|
break;
|
|
case PM_POST_SUSPEND:
|
|
list_for_each_entry(domain, &domains, list) {
|
|
policy = cpufreq_cpu_get(cpumask_any(&domain->cpus));
|
|
if (!policy)
|
|
continue;
|
|
if (__exynos_cpufreq_resume(policy, domain)) {
|
|
cpufreq_cpu_put(policy);
|
|
return NOTIFY_BAD;
|
|
}
|
|
cpufreq_cpu_put(policy);
|
|
}
|
|
break;
|
|
}
|
|
return NOTIFY_OK;
|
|
}
|
|
|
|
static struct notifier_block exynos_cpufreq_pm = {
|
|
.notifier_call = exynos_cpufreq_pm_notifier,
|
|
.priority = INT_MAX,
|
|
};
|
|
|
|
static struct cpufreq_driver exynos_driver = {
|
|
.name = "exynos_cpufreq",
|
|
.flags = CPUFREQ_STICKY | CPUFREQ_HAVE_GOVERNOR_PER_POLICY,
|
|
.init = exynos_cpufreq_init,
|
|
.verify = exynos_cpufreq_verify,
|
|
.target = exynos_cpufreq_target,
|
|
.get = exynos_cpufreq_get,
|
|
.fast_switch = exynos_cpufreq_fast_switch,
|
|
.resolve_freq = exynos_cpufreq_resolve_freq,
|
|
.suspend = exynos_cpufreq_suspend,
|
|
.resume = exynos_cpufreq_resume,
|
|
.ready = exynos_cpufreq_ready,
|
|
.attr = cpufreq_generic_attr,
|
|
};
|
|
|
|
/*********************************************************************
|
|
* CPUFREQ SYSFS *
|
|
*********************************************************************/
|
|
#define show_store_freq_qos(type) \
|
|
static ssize_t show_freq_qos_##type(struct device *dev, \
|
|
struct device_attribute *attr, char *buf) \
|
|
{ \
|
|
ssize_t count = 0; \
|
|
struct exynos_cpufreq_domain *domain; \
|
|
\
|
|
list_for_each_entry(domain, &domains, list) \
|
|
count += snprintf(buf + count, 30, \
|
|
"policy%d: qos_%s: %d\n", \
|
|
cpumask_first(&domain->cpus), #type, \
|
|
domain->user_##type##_qos_req.pnode.prio); \
|
|
\
|
|
return count; \
|
|
} \
|
|
\
|
|
static ssize_t store_freq_qos_##type(struct device *dev, \
|
|
struct device_attribute *attr, \
|
|
const char *buf, size_t count) \
|
|
{ \
|
|
int freq, cpu; \
|
|
struct exynos_cpufreq_domain *domain; \
|
|
\
|
|
if (!sscanf(buf, "%d %8d", &cpu, &freq)) \
|
|
return -EINVAL; \
|
|
\
|
|
if (cpu < 0 || cpu >= NR_CPUS || freq < 0) \
|
|
return -EINVAL; \
|
|
\
|
|
domain = find_domain(cpu); \
|
|
if (!domain) \
|
|
return -EINVAL; \
|
|
\
|
|
freq_qos_update_request(&domain->user_##type##_qos_req, freq); \
|
|
\
|
|
return count; \
|
|
}
|
|
|
|
show_store_freq_qos(min);
|
|
show_store_freq_qos(max);
|
|
|
|
static ssize_t show_dvfs_mode(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
ssize_t count = 0;
|
|
struct exynos_cpufreq_domain *domain;
|
|
|
|
list_for_each_entry(domain, &domains, list)
|
|
count += snprintf(buf + count, PAGE_SIZE, "policy%d: DVFS Mode: %s\n",
|
|
cpumask_first(&domain->cpus),
|
|
!!domain->dvfs_mode ? "BLOCKING":"NON_BLOCKING");
|
|
count += snprintf(buf + count, PAGE_SIZE, "Usage: echo [cpu] [mode] > dvfs_mode (mode = 0: NON_BLOCKING/1: BLOCKING)\n");
|
|
|
|
return count;
|
|
}
|
|
|
|
static ssize_t store_dvfs_mode(struct device *dev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
int mode, cpu;
|
|
struct exynos_cpufreq_domain *domain;
|
|
|
|
if (!sscanf(buf, "%d %8d", &cpu, &mode))
|
|
return -EINVAL;
|
|
|
|
if (cpu < 0 || cpu >= NR_CPUS || mode < 0)
|
|
return -EINVAL;
|
|
|
|
domain = find_domain(cpu);
|
|
if (!domain)
|
|
return -EINVAL;
|
|
|
|
domain->dvfs_mode = mode;
|
|
|
|
return count;
|
|
}
|
|
|
|
static DEVICE_ATTR(freq_qos_max, S_IRUGO | S_IWUSR,
|
|
show_freq_qos_max, store_freq_qos_max);
|
|
static DEVICE_ATTR(freq_qos_min, S_IRUGO | S_IWUSR,
|
|
show_freq_qos_min, store_freq_qos_min);
|
|
static DEVICE_ATTR(dvfs_mode, S_IRUGO | S_IWUSR,
|
|
show_dvfs_mode, store_dvfs_mode);
|
|
|
|
#if defined(CONFIG_ARM_EXYNOS_CLDVFS_SYSFS)
|
|
/****************************************************************/
|
|
/* CL-DVFS SYSFS INTERFACE */
|
|
/****************************************************************/
|
|
u32 cldc, cldp, cldol;
|
|
#define CLDVFS_BASE 0x1A320000
|
|
#define CLDVFS_CONTROL_OFFSET 0xE10
|
|
#define CLDVFS_PMICCAL_OFFSET 0xE14
|
|
#define CLDVFS_OUTERLOG_OFFSET 0xE18
|
|
|
|
static void __iomem *cldvfs_base;
|
|
|
|
static ssize_t cldc_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
u32 cldc_read = 0;
|
|
|
|
cldc_read = __raw_readl(cldvfs_base + CLDVFS_CONTROL_OFFSET);
|
|
|
|
return snprintf(buf, 30, "0x%x\n", cldc_read);
|
|
}
|
|
|
|
static ssize_t cldc_store(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
u32 input;
|
|
|
|
if (kstrtouint(buf, 0, &input))
|
|
return -EINVAL;
|
|
|
|
cldc = input;
|
|
__raw_writel(cldc, cldvfs_base + CLDVFS_CONTROL_OFFSET);
|
|
|
|
return count;
|
|
}
|
|
DEVICE_ATTR_RW(cldc);
|
|
|
|
static ssize_t cldp_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
u32 cldp_read = 0;
|
|
|
|
cldp_read = __raw_readl(cldvfs_base + CLDVFS_PMICCAL_OFFSET);
|
|
|
|
return snprintf(buf, 30, "0x%x\n", cldp_read);
|
|
}
|
|
|
|
static ssize_t cldp_store(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
u32 input;
|
|
|
|
if (kstrtouint(buf, 0, &input))
|
|
return -EINVAL;
|
|
|
|
cldp = input;
|
|
__raw_writel(cldp, cldvfs_base + CLDVFS_PMICCAL_OFFSET);
|
|
|
|
return count;
|
|
}
|
|
DEVICE_ATTR_RW(cldp);
|
|
|
|
static ssize_t cldol_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
u32 cldol_read = 0;
|
|
|
|
cldol_read = __raw_readl(cldvfs_base + CLDVFS_OUTERLOG_OFFSET);
|
|
|
|
return snprintf(buf, 30, "0x%x\n", cldol_read);
|
|
}
|
|
|
|
static ssize_t cldol_store(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
u32 input;
|
|
|
|
if (kstrtouint(buf, 0, &input))
|
|
return -EINVAL;
|
|
|
|
cldol = input;
|
|
__raw_writel(cldol, cldvfs_base + CLDVFS_OUTERLOG_OFFSET);
|
|
|
|
return count;
|
|
}
|
|
DEVICE_ATTR_RW(cldol);
|
|
|
|
static struct attribute *exynos_cldvfs_attrs[] = {
|
|
&dev_attr_cldc.attr,
|
|
&dev_attr_cldp.attr,
|
|
&dev_attr_cldol.attr,
|
|
NULL,
|
|
};
|
|
|
|
static struct attribute_group exynos_cldvfs_group = {
|
|
.name = "cldvfs",
|
|
.attrs = exynos_cldvfs_attrs,
|
|
};
|
|
#endif
|
|
|
|
/*********************************************************************
|
|
* TRANSITION NOTIFIER *
|
|
*********************************************************************/
|
|
static int exynos_cpufreq_fast_switch_count;
|
|
|
|
static void exynos_cpufreq_notify_transition_slow(struct cpufreq_policy *policy,
|
|
struct cpufreq_freqs *freqs,
|
|
unsigned int state,
|
|
int retval)
|
|
{
|
|
switch (state) {
|
|
case CPUFREQ_PRECHANGE:
|
|
cpufreq_freq_transition_begin(policy, freqs);
|
|
break;
|
|
|
|
case CPUFREQ_POSTCHANGE:
|
|
cpufreq_freq_transition_end(policy, freqs, retval);
|
|
break;
|
|
|
|
default:
|
|
BUG();
|
|
}
|
|
}
|
|
|
|
static void exynos_cpufreq_notify_transition(struct cpufreq_policy *policy,
|
|
struct cpufreq_freqs *freqs,
|
|
unsigned int state,
|
|
int retval)
|
|
{
|
|
if (exynos_cpufreq_fast_switch_count == 0) {
|
|
exynos_cpufreq_notify_transition_slow(policy, freqs, state, retval);
|
|
return;
|
|
}
|
|
|
|
read_lock(&exynos_cpufreq_transition_notifier_lock);
|
|
|
|
switch (state) {
|
|
case CPUFREQ_PRECHANGE:
|
|
raw_notifier_call_chain(&exynos_cpufreq_transition_notifier_list,
|
|
CPUFREQ_PRECHANGE, freqs);
|
|
break;
|
|
|
|
case CPUFREQ_POSTCHANGE:
|
|
raw_notifier_call_chain(&exynos_cpufreq_transition_notifier_list,
|
|
CPUFREQ_POSTCHANGE, freqs);
|
|
|
|
/* Transition failed */
|
|
if (retval) {
|
|
swap(freqs->old, freqs->new);
|
|
raw_notifier_call_chain(&exynos_cpufreq_transition_notifier_list,
|
|
CPUFREQ_PRECHANGE, freqs);
|
|
raw_notifier_call_chain(&exynos_cpufreq_transition_notifier_list,
|
|
CPUFREQ_POSTCHANGE, freqs);
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
BUG();
|
|
}
|
|
|
|
read_unlock(&exynos_cpufreq_transition_notifier_lock);
|
|
}
|
|
|
|
int exynos_cpufreq_register_notifier(struct notifier_block *nb, unsigned int list)
|
|
{
|
|
int ret;
|
|
unsigned long flags;
|
|
|
|
if (exynos_cpufreq_fast_switch_count == 0)
|
|
return cpufreq_register_notifier(nb, list);
|
|
|
|
write_lock_irqsave(&exynos_cpufreq_transition_notifier_lock, flags);
|
|
|
|
switch (list) {
|
|
case CPUFREQ_TRANSITION_NOTIFIER:
|
|
ret = raw_notifier_chain_register(
|
|
&exynos_cpufreq_transition_notifier_list, nb);
|
|
break;
|
|
default:
|
|
pr_warn("Unsupported notifier (list=%u)\n", list);
|
|
ret = -EINVAL;
|
|
}
|
|
|
|
write_unlock_irqrestore(&exynos_cpufreq_transition_notifier_lock, flags);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL(exynos_cpufreq_register_notifier);
|
|
|
|
int exynos_cpufreq_unregister_notifier(struct notifier_block *nb, unsigned int list)
|
|
{
|
|
int ret;
|
|
unsigned long flags;
|
|
|
|
if (exynos_cpufreq_fast_switch_count == 0)
|
|
return cpufreq_unregister_notifier(nb, list);
|
|
|
|
write_lock_irqsave(&exynos_cpufreq_transition_notifier_lock, flags);
|
|
|
|
switch (list) {
|
|
case CPUFREQ_TRANSITION_NOTIFIER:
|
|
ret = raw_notifier_chain_unregister(
|
|
&exynos_cpufreq_transition_notifier_list, nb);
|
|
break;
|
|
default:
|
|
pr_warn("Unsupported notifier (list=%u)\n", list);
|
|
ret = -EINVAL;
|
|
}
|
|
|
|
write_unlock_irqrestore(&exynos_cpufreq_transition_notifier_lock, flags);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL(exynos_cpufreq_unregister_notifier);
|
|
|
|
/*********************************************************************
|
|
* CPUFREQ DEV FOPS *
|
|
*********************************************************************/
|
|
|
|
static ssize_t cpufreq_fops_write(struct file *filp, const char __user *buf,
|
|
size_t count, loff_t *f_pos)
|
|
{
|
|
s32 value;
|
|
struct freq_qos_request *req = filp->private_data;
|
|
if (count == sizeof(s32)) {
|
|
if (copy_from_user(&value, buf, sizeof(s32)))
|
|
return -EFAULT;
|
|
} else {
|
|
int ret;
|
|
|
|
ret = kstrtos32_from_user(buf, count, 16, &value);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
freq_qos_update_request(req, value);
|
|
|
|
return count;
|
|
}
|
|
|
|
static ssize_t cpufreq_fops_read(struct file *filp, char __user *buf,
|
|
size_t count, loff_t *f_pos)
|
|
{
|
|
s32 value = 0;
|
|
return simple_read_from_buffer(buf, count, f_pos, &value, sizeof(s32));
|
|
}
|
|
|
|
static int cpufreq_fops_open(struct inode *inode, struct file *filp)
|
|
{
|
|
int ret;
|
|
struct exynos_cpufreq_file_operations *fops = container_of(filp->f_op,
|
|
struct exynos_cpufreq_file_operations,
|
|
fops);
|
|
struct freq_qos_request *req = kzalloc(sizeof(*req), GFP_KERNEL);
|
|
if (!req)
|
|
return -ENOMEM;
|
|
|
|
filp->private_data = req;
|
|
ret = freq_qos_tracer_add_request(fops->freq_constraints, req, fops->req_type, fops->default_value);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cpufreq_fops_release(struct inode *inode, struct file *filp)
|
|
{
|
|
struct freq_qos_request *req = filp->private_data;
|
|
|
|
freq_qos_tracer_remove_request(req);
|
|
kfree(req);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*********************************************************************
|
|
* MULTI DM TABLE *
|
|
*********************************************************************/
|
|
static struct exynos_cpufreq_dm *
|
|
find_multi_table_dm(struct list_head *dm_list)
|
|
{
|
|
struct exynos_cpufreq_dm *dm;
|
|
|
|
list_for_each_entry(dm, dm_list, list)
|
|
if (dm->multi_table)
|
|
return dm;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
struct work_struct dm_work;
|
|
static int dm_table_index;
|
|
static int cur_type;
|
|
static void change_dm_table_work(struct work_struct *work)
|
|
{
|
|
struct exynos_cpufreq_domain *domain;
|
|
struct exynos_cpufreq_dm *dm;
|
|
int index = 0;
|
|
|
|
if ((cur_type & EMSTUNE_MODE_TYPE_GAME) &&
|
|
!(cur_type & EMSTUNE_BOOST_TYPE_EXTREME))
|
|
index = 1;
|
|
|
|
if (dm_table_index != index) {
|
|
list_for_each_entry(domain, &domains, list) {
|
|
dm = find_multi_table_dm(&domain->dm_list);
|
|
if (dm)
|
|
exynos_dm_change_freq_table(&dm->c, index);
|
|
}
|
|
dm_table_index = index;
|
|
}
|
|
}
|
|
|
|
static int exynos_cpufreq_mode_update_callback(struct notifier_block *nb,
|
|
unsigned long val, void *v)
|
|
{
|
|
cur_type = emstune_cpu_dsu_table_index(v);
|
|
|
|
schedule_work(&dm_work);
|
|
|
|
return NOTIFY_OK;
|
|
}
|
|
static struct notifier_block exynos_cpufreq_mode_update_notifier = {
|
|
.notifier_call = exynos_cpufreq_mode_update_callback,
|
|
};
|
|
|
|
/*********************************************************************
|
|
* EXTERNAL EVENT HANDLER *
|
|
*********************************************************************/
|
|
static int exynos_cpu_fast_switch_notifier(struct notifier_block *notifier,
|
|
unsigned long domain_id, void *__data)
|
|
{
|
|
struct exynos_cpufreq_domain *domain = NULL;
|
|
struct exynos_dm_fast_switch_notify_data *data = __data;
|
|
unsigned int freq = data->freq;
|
|
ktime_t time = data->time;
|
|
|
|
list_for_each_entry(domain, &domains, list)
|
|
if ((domain->cal_id & 0xffff) == domain_id)
|
|
break;
|
|
|
|
if (unlikely(!domain))
|
|
return NOTIFY_DONE;
|
|
|
|
if (!domain->fast_switch_possible || domain->dvfs_mode == BLOCKING)
|
|
return NOTIFY_BAD;
|
|
|
|
trace_acme_scale_freq(domain->id, 0, freq, "end", (unsigned long)ktime_to_us(time));
|
|
|
|
return NOTIFY_OK;
|
|
}
|
|
|
|
static struct notifier_block exynos_cpu_fast_switch_nb = {
|
|
.notifier_call = exynos_cpu_fast_switch_notifier,
|
|
};
|
|
|
|
/*********************************************************************
|
|
* SUPPORT for DVFS MANAGER *
|
|
*********************************************************************/
|
|
static int
|
|
init_constraint_table_ect(struct exynos_dm_freq *dm_table, int table_length,
|
|
struct device_node *dn)
|
|
{
|
|
void *block;
|
|
struct ect_minlock_domain *ect_domain;
|
|
const char *ect_name;
|
|
unsigned int index, c_index;
|
|
bool valid_row = false;
|
|
int ret;
|
|
|
|
ret = of_property_read_string(dn, "ect-name", &ect_name);
|
|
if (ret)
|
|
return ret;
|
|
|
|
block = ect_get_block(BLOCK_MINLOCK);
|
|
if (!block)
|
|
return -ENODEV;
|
|
|
|
ect_domain = ect_minlock_get_domain(block, (char *)ect_name);
|
|
if (!ect_domain)
|
|
return -ENODEV;
|
|
|
|
for (index = 0; index < table_length; index++) {
|
|
unsigned int freq = dm_table[index].master_freq;
|
|
|
|
for (c_index = 0; c_index < ect_domain->num_of_level; c_index++) {
|
|
/* find row same as frequency */
|
|
if (freq == ect_domain->level[c_index].main_frequencies) {
|
|
dm_table[index].slave_freq
|
|
= ect_domain->level[c_index].sub_frequencies;
|
|
valid_row = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Due to higher levels of constraint_freq should not be NULL,
|
|
* they should be filled with highest value of sub_frequencies
|
|
* of ect until finding first(highest) domain frequency fit with
|
|
* main_frequeucy of ect.
|
|
*/
|
|
if (!valid_row)
|
|
dm_table[index].slave_freq
|
|
= ect_domain->level[0].sub_frequencies;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
init_constraint_table_dt(struct exynos_dm_freq *dm_table, int table_length,
|
|
struct device_node *dn)
|
|
{
|
|
struct exynos_dm_freq *table;
|
|
int size, table_size, index, c_index;
|
|
|
|
/*
|
|
* A DM constraint table row consists of master and slave frequency
|
|
* value, the size of a row is 64bytes. Divide size in half when
|
|
* table is allocated.
|
|
*/
|
|
size = of_property_count_u32_elems(dn, "table");
|
|
if (size < 0)
|
|
return size;
|
|
|
|
table_size = size / 2;
|
|
table = kzalloc(sizeof(struct exynos_dm_freq) * table_size, GFP_KERNEL);
|
|
if (!table)
|
|
return -ENOMEM;
|
|
|
|
of_property_read_u32_array(dn, "table", (unsigned int *)table, size);
|
|
|
|
for (index = 0; index < table_length; index++) {
|
|
unsigned int freq = dm_table[index].master_freq;
|
|
|
|
for (c_index = 0; c_index < table_size; c_index++) {
|
|
if (freq <= table[c_index].master_freq) {
|
|
dm_table[index].slave_freq
|
|
= table[c_index].slave_freq;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
kfree(table);
|
|
return 0;
|
|
}
|
|
|
|
void adjust_freq_table_with_min_max_freq(unsigned int *freq_table,
|
|
int table_size, struct exynos_cpufreq_domain *domain)
|
|
{
|
|
int index;
|
|
|
|
for (index = 0; index < table_size; index++) {
|
|
if (freq_table[index] == domain->min_freq)
|
|
break;
|
|
|
|
if (freq_table[index] > domain->min_freq) {
|
|
freq_table[index - 1] = domain->min_freq;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (index = table_size - 1; index >= 0; index--) {
|
|
if (freq_table[index] == domain->max_freq)
|
|
return;
|
|
|
|
if (freq_table[index] < domain->max_freq) {
|
|
freq_table[index + 1] = domain->max_freq;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int dm_scaler(int dm_type, void *devdata, unsigned int target_freq,
|
|
unsigned int relation)
|
|
{
|
|
struct exynos_cpufreq_domain *domain = devdata;
|
|
struct cpufreq_policy *policy;
|
|
struct cpumask mask;
|
|
int ret;
|
|
|
|
/* Skip scaling if all cpus of domain are hotplugged out */
|
|
cpumask_and(&mask, &domain->cpus, cpu_online_mask);
|
|
if (cpumask_empty(&mask))
|
|
return -ENODEV;
|
|
|
|
policy = cpufreq_cpu_get(cpumask_first(&mask));
|
|
if (!policy) {
|
|
pr_err("%s: failed get cpufreq policy\n", __func__);
|
|
return -ENODEV;
|
|
}
|
|
|
|
ret = __exynos_cpufreq_target(policy, target_freq, relation);
|
|
|
|
cpufreq_cpu_put(policy);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int init_dm(struct exynos_cpufreq_domain *domain,
|
|
struct device_node *dn)
|
|
{
|
|
struct exynos_cpufreq_dm *dm;
|
|
struct device_node *root;
|
|
struct of_phandle_iterator iter;
|
|
int ret, err;
|
|
|
|
ret = of_property_read_u32(dn, "dm-type", &domain->dm_type);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = exynos_dm_data_init(domain->dm_type, domain, domain->min_freq,
|
|
domain->max_freq, domain->min_freq);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Initialize list head of DVFS Manager constraints */
|
|
INIT_LIST_HEAD(&domain->dm_list);
|
|
|
|
/*
|
|
* Initialize DVFS Manager constraints
|
|
* - constraint_type : minimum or maximum constraint
|
|
* - constraint_dm_type : cpu/mif/int/.. etc
|
|
* - guidance : constraint from chipset characteristic
|
|
* - freq_table : constraint table
|
|
*/
|
|
root = of_get_child_by_name(dn, "dm-constraints");
|
|
of_for_each_phandle(&iter, err, root, "list", NULL, 0) {
|
|
struct exynos_dm_freq *dm_table;
|
|
int index, r_index;
|
|
bool multi_table = false;
|
|
|
|
if (of_property_read_bool(iter.node, "multi-table"))
|
|
multi_table = true;
|
|
|
|
if (multi_table) {
|
|
dm = find_multi_table_dm(&domain->dm_list);
|
|
if (dm)
|
|
goto skip_init_dm;
|
|
}
|
|
|
|
/* allocate DM constraint */
|
|
dm = kzalloc(sizeof(struct exynos_cpufreq_dm), GFP_KERNEL);
|
|
if (!dm)
|
|
goto init_fail;
|
|
|
|
list_add_tail(&dm->list, &domain->dm_list);
|
|
|
|
of_property_read_u32(iter.node, "const-type", &dm->c.constraint_type);
|
|
of_property_read_u32(iter.node, "dm-slave", &dm->c.dm_slave);
|
|
of_property_read_u32(iter.node, "master-cal-id", &dm->master_cal_id);
|
|
of_property_read_u32(iter.node, "slave-cal-id", &dm->slave_cal_id);
|
|
|
|
/* dynamic disable for migov control */
|
|
if (of_property_read_bool(iter.node, "dynamic-disable"))
|
|
dm->c.support_dynamic_disable = true;
|
|
|
|
dm->multi_table = multi_table;
|
|
|
|
skip_init_dm:
|
|
/* allocate DM constraint table */
|
|
dm_table = kcalloc(domain->table_size, sizeof(struct exynos_dm_freq), GFP_KERNEL);
|
|
if (!dm_table)
|
|
goto init_fail;
|
|
|
|
/*
|
|
* fill master freq, domain frequency table is in ascending
|
|
* order, but DM constraint table must be in descending
|
|
* order.
|
|
*/
|
|
index = 0;
|
|
r_index = domain->table_size - 1;
|
|
while (r_index >= 0) {
|
|
dm_table[index].master_freq =
|
|
domain->freq_table[r_index].frequency;
|
|
index++;
|
|
r_index--;
|
|
}
|
|
|
|
/* fill slave freq */
|
|
if (of_property_read_bool(iter.node, "guidance")) {
|
|
dm->c.guidance = true;
|
|
if (init_constraint_table_ect(dm_table,
|
|
domain->table_size, iter.node))
|
|
continue;
|
|
} else {
|
|
if (init_constraint_table_dt(dm_table,
|
|
domain->table_size, iter.node))
|
|
continue;
|
|
}
|
|
|
|
dm->c.table_length = domain->table_size;
|
|
|
|
if (dm->multi_table) {
|
|
/*
|
|
* DM supports only 2 variable_freq_table.
|
|
* It should support table extension.
|
|
*/
|
|
if (!dm->c.variable_freq_table[0]) {
|
|
dm->c.variable_freq_table[0] = dm_table;
|
|
|
|
/*
|
|
* Do not register DM constraint
|
|
* unless both tables are initialized
|
|
*/
|
|
continue;
|
|
} else
|
|
dm->c.variable_freq_table[1] = dm_table;
|
|
|
|
dm->c.support_variable_freq_table = true;
|
|
} else
|
|
dm->c.freq_table = dm_table;
|
|
|
|
/* register DM constraint */
|
|
ret = register_exynos_dm_constraint_table(domain->dm_type, &dm->c);
|
|
if (ret)
|
|
goto init_fail;
|
|
}
|
|
|
|
return register_exynos_dm_freq_scaler(domain->dm_type, dm_scaler);
|
|
|
|
init_fail:
|
|
while (!list_empty(&domain->dm_list)) {
|
|
dm = list_last_entry(&domain->dm_list,
|
|
struct exynos_cpufreq_dm, list);
|
|
list_del(&dm->list);
|
|
kfree(dm->c.freq_table);
|
|
kfree(dm);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_BOOTSTAT)
|
|
void sec_bootstat_get_cpuinfo(int *freq, int *online)
|
|
{
|
|
int cpu;
|
|
int cluster;
|
|
struct exynos_cpufreq_domain *domain;
|
|
|
|
get_online_cpus();
|
|
*online = cpumask_bits(cpu_online_mask)[0];
|
|
for_each_online_cpu(cpu) {
|
|
domain = find_domain(cpu);
|
|
if (!domain)
|
|
continue;
|
|
pr_err("%s, dm type = %d\n", __func__, domain->dm_type);
|
|
cluster = 0;
|
|
if (domain->dm_type == DM_CPU_CL1)
|
|
cluster = 1;
|
|
else if (domain->dm_type == DM_CPU_CL2)
|
|
cluster = 2;
|
|
|
|
freq[cluster] = get_freq(domain);
|
|
}
|
|
put_online_cpus();
|
|
}
|
|
EXPORT_SYMBOL(sec_bootstat_get_cpuinfo);
|
|
#endif
|
|
|
|
/*********************************************************************
|
|
* CPU HOTPLUG CALLBACK *
|
|
*********************************************************************/
|
|
static int exynos_cpufreq_cpu_up_callback(unsigned int cpu)
|
|
{
|
|
struct exynos_cpufreq_domain *domain;
|
|
struct cpumask mask;
|
|
|
|
/*
|
|
* CPU frequency is not changed before cpufreq_resume() is called.
|
|
* Therefore, if it is called by enable_nonboot_cpus(),
|
|
* it is ignored.
|
|
*/
|
|
if (cpuhp_tasks_frozen)
|
|
return 0;
|
|
|
|
domain = find_domain(cpu);
|
|
if (!domain)
|
|
return 0;
|
|
|
|
/*
|
|
* The first incoming cpu in domain enables frequency scaling
|
|
* and clears limit of frequency.
|
|
*/
|
|
cpumask_and(&mask, &domain->cpus, cpu_online_mask);
|
|
if (cpumask_weight(&mask) == 1) {
|
|
enable_domain(domain);
|
|
freq_qos_update_request(&domain->max_qos_req, domain->max_freq);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int exynos_cpufreq_cpu_down_callback(unsigned int cpu)
|
|
{
|
|
struct exynos_cpufreq_domain *domain;
|
|
struct cpumask mask;
|
|
|
|
/*
|
|
* CPU frequency is not changed after cpufreq_suspend() is called.
|
|
* Therefore, if it is called by disable_nonboot_cpus(),
|
|
* it is ignored.
|
|
*/
|
|
if (cpuhp_tasks_frozen)
|
|
return 0;
|
|
|
|
domain = find_domain(cpu);
|
|
if (!domain)
|
|
return 0;
|
|
|
|
/*
|
|
* The last outgoing cpu in domain limits frequency to minimum
|
|
* and disables frequency scaling.
|
|
*/
|
|
cpumask_and(&mask, &domain->cpus, cpu_online_mask);
|
|
if (cpumask_weight(&mask) == 1) {
|
|
freq_qos_update_request(&domain->max_qos_req, domain->min_freq);
|
|
disable_domain(domain);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*********************************************************************
|
|
* INITIALIZE EXYNOS CPUFREQ DRIVER *
|
|
*********************************************************************/
|
|
static void print_domain_info(struct exynos_cpufreq_domain *domain)
|
|
{
|
|
int i;
|
|
char buf[10];
|
|
|
|
pr_info("CPUFREQ of domain%d cal-id : %#x\n",
|
|
domain->id, domain->cal_id);
|
|
|
|
scnprintf(buf, sizeof(buf), "%*pbl", cpumask_pr_args(&domain->cpus));
|
|
pr_info("CPUFREQ of domain%d sibling cpus : %s\n",
|
|
domain->id, buf);
|
|
|
|
pr_info("CPUFREQ of domain%d boot freq = %d kHz resume freq = %d kHz\n",
|
|
domain->id, domain->boot_freq, domain->resume_freq);
|
|
|
|
pr_info("CPUFREQ of domain%d max freq : %d kHz, min freq : %d kHz\n",
|
|
domain->id,
|
|
domain->max_freq, domain->min_freq);
|
|
|
|
pr_info("CPUFREQ of domain%d table size = %d\n",
|
|
domain->id, domain->table_size);
|
|
|
|
for (i = 0; i < domain->table_size; i++) {
|
|
if (domain->freq_table[i].frequency == CPUFREQ_ENTRY_INVALID)
|
|
continue;
|
|
|
|
pr_info("CPUFREQ of domain%d : L%-2d %7d kHz\n",
|
|
domain->id,
|
|
domain->freq_table[i].driver_data,
|
|
domain->freq_table[i].frequency);
|
|
}
|
|
}
|
|
|
|
static void init_sysfs(struct kobject *kobj)
|
|
{
|
|
if (sysfs_create_file(kobj, &dev_attr_freq_qos_max.attr))
|
|
pr_err("failed to create user_max node\n");
|
|
|
|
if (sysfs_create_file(kobj, &dev_attr_freq_qos_min.attr))
|
|
pr_err("failed to create user_min node\n");
|
|
|
|
if (sysfs_create_file(kobj, &dev_attr_dvfs_mode.attr))
|
|
pr_err("failed to create dvfs_mode node\n");
|
|
}
|
|
|
|
static void freq_qos_release(struct work_struct *work)
|
|
{
|
|
struct exynos_cpufreq_domain *domain = container_of(to_delayed_work(work),
|
|
struct exynos_cpufreq_domain,
|
|
work);
|
|
|
|
freq_qos_update_request(&domain->min_qos_req, domain->min_freq);
|
|
freq_qos_update_request(&domain->max_qos_req, domain->max_freq);
|
|
}
|
|
|
|
static int
|
|
init_freq_qos(struct exynos_cpufreq_domain *domain, struct cpufreq_policy *policy)
|
|
{
|
|
unsigned int boot_qos, val;
|
|
struct device_node *dn = domain->dn;
|
|
int ret;
|
|
|
|
ret = freq_qos_tracer_add_request(&policy->constraints, &domain->min_qos_req,
|
|
FREQ_QOS_MIN, domain->min_freq);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = freq_qos_tracer_add_request(&policy->constraints, &domain->max_qos_req,
|
|
FREQ_QOS_MAX, domain->max_freq);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = freq_qos_tracer_add_request(&policy->constraints, &domain->user_min_qos_req,
|
|
FREQ_QOS_MIN, domain->min_freq);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = freq_qos_tracer_add_request(&policy->constraints, &domain->user_max_qos_req,
|
|
FREQ_QOS_MAX, domain->max_freq);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/*
|
|
* Basically booting pm_qos is set to max frequency of domain.
|
|
* But if pm_qos-booting exists in device tree,
|
|
* booting pm_qos is selected to smaller one
|
|
* between max frequency of domain and the value defined in device tree.
|
|
*/
|
|
boot_qos = domain->max_freq;
|
|
if (!of_property_read_u32(dn, "pm_qos-booting", &val))
|
|
boot_qos = min(boot_qos, val);
|
|
|
|
#if defined(CONFIG_SEC_FACTORY)
|
|
pr_info("%s: flexable_cpu_boot = %d\n", __func__, flexable_cpu_boot);
|
|
|
|
if (flexable_cpu_boot == FLEXBOOT_ALL) {
|
|
pr_info("All skip boot cpu[%d] max qos lock\n", domain->id);
|
|
} else if ((flexable_cpu_boot >= FLEXBOOT_LIT) && (flexable_cpu_boot <= FLEXBOOT_BIG)) {
|
|
if (domain->id == (flexable_cpu_boot - 1))
|
|
pr_info("skip boot cpu[%d] max qos lock\n", domain->id);
|
|
else
|
|
freq_qos_update_request(&domain->max_qos_req, boot_qos);
|
|
} else if (flexable_cpu_boot == FLEXBOOT_FLEX_ONLY) {
|
|
freq_qos_update_request(&domain->max_qos_req, boot_qos);
|
|
} else {
|
|
freq_qos_update_request(&domain->min_qos_req, boot_qos);
|
|
freq_qos_update_request(&domain->max_qos_req, boot_qos);
|
|
}
|
|
#else
|
|
freq_qos_update_request(&domain->min_qos_req, boot_qos);
|
|
freq_qos_update_request(&domain->max_qos_req, boot_qos);
|
|
#endif
|
|
pr_info("domain%d operates at %dKHz for %d secs\n", domain->id, boot_qos, BOOTING_BOOST_TIME/1000);
|
|
|
|
/* booting boost, it is expired after BOOTING_BOOST_TIME */
|
|
INIT_DELAYED_WORK(&domain->work, freq_qos_release);
|
|
schedule_delayed_work(&domain->work, msecs_to_jiffies(BOOTING_BOOST_TIME));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
init_fops(struct exynos_cpufreq_domain *domain, struct cpufreq_policy *policy)
|
|
{
|
|
char *node_name_buffer;
|
|
int ret, buffer_size;;
|
|
|
|
buffer_size = sizeof(char [64]);
|
|
node_name_buffer = kzalloc(buffer_size, GFP_KERNEL);
|
|
if (node_name_buffer == NULL)
|
|
return -ENOMEM;
|
|
|
|
snprintf(node_name_buffer, buffer_size,
|
|
"cluster%d_freq_min", domain->id);
|
|
|
|
domain->min_qos_fops.fops.write = cpufreq_fops_write;
|
|
domain->min_qos_fops.fops.read = cpufreq_fops_read;
|
|
domain->min_qos_fops.fops.open = cpufreq_fops_open;
|
|
domain->min_qos_fops.fops.release = cpufreq_fops_release;
|
|
domain->min_qos_fops.fops.llseek = noop_llseek;
|
|
|
|
domain->min_qos_fops.miscdev.minor = MISC_DYNAMIC_MINOR;
|
|
domain->min_qos_fops.miscdev.name = node_name_buffer;
|
|
domain->min_qos_fops.miscdev.fops = &domain->min_qos_fops.fops;
|
|
|
|
domain->min_qos_fops.freq_constraints = &policy->constraints;
|
|
domain->min_qos_fops.default_value = FREQ_QOS_MIN_DEFAULT_VALUE;
|
|
domain->min_qos_fops.req_type = FREQ_QOS_MIN;
|
|
|
|
ret = misc_register(&domain->min_qos_fops.miscdev);
|
|
if (ret) {
|
|
pr_err("CPUFREQ couldn't register misc device min for domain %d", domain->id);
|
|
kfree(node_name_buffer);
|
|
return ret;
|
|
}
|
|
|
|
node_name_buffer = kzalloc(buffer_size, GFP_KERNEL);
|
|
if (node_name_buffer == NULL)
|
|
return -ENOMEM;
|
|
|
|
snprintf(node_name_buffer, buffer_size,
|
|
"cluster%d_freq_max", domain->id);
|
|
|
|
domain->max_qos_fops.fops.write = cpufreq_fops_write;
|
|
domain->max_qos_fops.fops.read = cpufreq_fops_read;
|
|
domain->max_qos_fops.fops.open = cpufreq_fops_open;
|
|
domain->max_qos_fops.fops.release = cpufreq_fops_release;
|
|
domain->max_qos_fops.fops.llseek = noop_llseek;
|
|
|
|
domain->max_qos_fops.miscdev.minor = MISC_DYNAMIC_MINOR;
|
|
domain->max_qos_fops.miscdev.name = node_name_buffer;
|
|
domain->max_qos_fops.miscdev.fops = &domain->max_qos_fops.fops;
|
|
|
|
domain->max_qos_fops.freq_constraints = &policy->constraints;
|
|
domain->max_qos_fops.default_value = FREQ_QOS_MAX_DEFAULT_VALUE;
|
|
domain->max_qos_fops.req_type = FREQ_QOS_MAX;
|
|
|
|
ret = misc_register(&domain->max_qos_fops.miscdev);
|
|
if (ret) {
|
|
pr_err("CPUFREQ couldn't register misc device max for domain %d", domain->id);
|
|
kfree(node_name_buffer);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
struct freq_volt {
|
|
unsigned int freq;
|
|
unsigned int volt;
|
|
};
|
|
|
|
static void exynos_cpufreq_irq_work(struct irq_work *irq_work)
|
|
{
|
|
struct exynos_cpufreq_domain *domain;
|
|
|
|
domain = container_of(irq_work,
|
|
struct exynos_cpufreq_domain,
|
|
fast_switch_irq_work);
|
|
if (unlikely(!domain))
|
|
return;
|
|
|
|
kthread_queue_work(&domain->fast_switch_worker,
|
|
&domain->fast_switch_work);
|
|
}
|
|
|
|
static void exynos_cpufreq_work(struct kthread_work *work)
|
|
{
|
|
struct exynos_cpufreq_domain *domain;
|
|
unsigned long flags;
|
|
unsigned long freq;
|
|
|
|
domain = container_of(work, struct exynos_cpufreq_domain, fast_switch_work);
|
|
if (unlikely(!domain))
|
|
return;
|
|
|
|
raw_spin_lock_irqsave(&domain->fast_switch_update_lock, flags);
|
|
freq = (unsigned long)domain->cached_fast_switch_freq;
|
|
domain->work_in_progress = false;
|
|
raw_spin_unlock_irqrestore(&domain->fast_switch_update_lock, flags);
|
|
|
|
if (DM_CALL(domain->dm_type, &freq))
|
|
trace_acme_scale_freq(domain->id, domain->old, freq, "fail", 0);
|
|
}
|
|
|
|
static int init_fast_switch(struct exynos_cpufreq_domain *domain,
|
|
struct device_node *dn)
|
|
{
|
|
struct task_struct *thread;
|
|
struct sched_param param = { .sched_priority = MAX_USER_RT_PRIO / 2 };
|
|
struct cpumask mask;
|
|
const char *buf;
|
|
int ret;
|
|
|
|
domain->fast_switch_possible = of_property_read_bool(dn, "fast-switch");
|
|
if (!domain->fast_switch_possible)
|
|
return 0;
|
|
|
|
init_irq_work(&domain->fast_switch_irq_work, exynos_cpufreq_irq_work);
|
|
kthread_init_work(&domain->fast_switch_work, exynos_cpufreq_work);
|
|
kthread_init_worker(&domain->fast_switch_worker);
|
|
thread = kthread_create(kthread_worker_fn, &domain->fast_switch_worker,
|
|
"fast_switch:%d", cpumask_first(&domain->cpus));
|
|
if (IS_ERR(thread)) {
|
|
pr_err("failed to create fast_switch thread: %ld\n", PTR_ERR(thread));
|
|
return PTR_ERR(thread);
|
|
}
|
|
|
|
ret = sched_setscheduler_nocheck(thread, SCHED_FIFO, ¶m);
|
|
if (ret) {
|
|
kthread_stop(thread);
|
|
pr_warn("%s: failed to set SCHED_CLASS\n", __func__);
|
|
return ret;
|
|
}
|
|
|
|
cpumask_copy(&mask, cpu_possible_mask);
|
|
if (!of_property_read_string(dn, "thread-run-on", &buf))
|
|
cpulist_parse(buf, &mask);
|
|
|
|
set_cpus_allowed_ptr(thread, &mask);
|
|
thread->flags |= PF_NO_SETAFFINITY;
|
|
|
|
raw_spin_lock_init(&domain->fast_switch_update_lock);
|
|
domain->thread = thread;
|
|
|
|
wake_up_process(thread);
|
|
|
|
exynos_cpufreq_fast_switch_count++;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int init_domain(struct exynos_cpufreq_domain *domain,
|
|
struct device_node *dn)
|
|
{
|
|
unsigned int val, raw_table_size;
|
|
int index, i;
|
|
unsigned int freq_table[100];
|
|
struct freq_volt *fv_table;
|
|
const char *buf;
|
|
int cpu;
|
|
int ret;
|
|
int cur_idx;
|
|
|
|
/*
|
|
* Get cpumask which belongs to domain.
|
|
*/
|
|
ret = of_property_read_string(dn, "sibling-cpus", &buf);
|
|
if (ret)
|
|
return ret;
|
|
|
|
cpulist_parse(buf, &domain->cpus);
|
|
cpumask_and(&domain->cpus, &domain->cpus, cpu_possible_mask);
|
|
if (cpumask_weight(&domain->cpus) == 0)
|
|
return -ENODEV;
|
|
|
|
/* Get CAL ID */
|
|
ret = of_property_read_u32(dn, "cal-id", &domain->cal_id);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Get DSS type */
|
|
if (of_property_read_u32(dn, "dss-type", &domain->dss_type)) {
|
|
pr_info("%s:dss_type is not initialized. domain_id replaces it.", __func__);
|
|
domain->dss_type = domain->id;
|
|
}
|
|
|
|
/*
|
|
* Set min/max frequency.
|
|
* If max-freq property exists in device tree, max frequency is
|
|
* selected to smaller one between the value defined in device
|
|
* tree and CAL. In case of min-freq, min frequency is selected
|
|
* to bigger one.
|
|
*/
|
|
domain->max_freq = cal_dfs_get_max_freq(domain->cal_id);
|
|
domain->min_freq = cal_dfs_get_min_freq(domain->cal_id);
|
|
|
|
if (!of_property_read_u32(dn, "max-freq", &val))
|
|
domain->max_freq = min(domain->max_freq, val);
|
|
if (!of_property_read_u32(dn, "min-freq", &val))
|
|
domain->min_freq = max(domain->min_freq, val);
|
|
|
|
if (domain->id == 1) { // BIG
|
|
domain->boot_freq = 2400000;
|
|
} else if (domain->id == 0) {
|
|
domain->boot_freq = 2002000;
|
|
}
|
|
|
|
/* Get freq-table from device tree and cut the out of range */
|
|
raw_table_size = of_property_count_u32_elems(dn, "freq-table");
|
|
if (of_property_read_u32_array(dn, "freq-table",
|
|
freq_table, raw_table_size)) {
|
|
pr_err("%s: freq-table does not exist\n", __func__);
|
|
return -ENODATA;
|
|
}
|
|
|
|
/*
|
|
* If the ECT's min/max frequency are asynchronous with the dts',
|
|
* adjust the freq table with the ECT's min/max frequency.
|
|
* It only supports the situations when the ECT's min is higher than the dts'
|
|
* or the ECT's max is lower than the dts'.
|
|
*/
|
|
adjust_freq_table_with_min_max_freq(freq_table, raw_table_size, domain);
|
|
|
|
domain->table_size = 0;
|
|
for (index = 0; index < raw_table_size; index++) {
|
|
if (freq_table[index] > domain->max_freq ||
|
|
freq_table[index] < domain->min_freq) {
|
|
freq_table[index] = CPUFREQ_ENTRY_INVALID;
|
|
continue;
|
|
}
|
|
|
|
domain->table_size++;
|
|
}
|
|
|
|
/*
|
|
* Get volt table from CAL with given freq-table
|
|
* cal_dfs_get_freq_volt_table() is called by filling the desired
|
|
* frequency in fv_table, the corresponding volt is filled.
|
|
*/
|
|
fv_table = kzalloc(sizeof(struct freq_volt)
|
|
* (domain->table_size), GFP_KERNEL);
|
|
if (!fv_table) {
|
|
pr_err("%s: failed to alloc fv_table\n", __func__);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
i = 0;
|
|
for (index = 0; index < raw_table_size; index++) {
|
|
if (freq_table[index] == CPUFREQ_ENTRY_INVALID)
|
|
continue;
|
|
fv_table[i].freq = freq_table[index];
|
|
i++;
|
|
}
|
|
|
|
if (cal_dfs_get_freq_volt_table(domain->cal_id,
|
|
fv_table, domain->table_size)) {
|
|
pr_err("%s: failed to get fv table from CAL\n", __func__);
|
|
kfree(fv_table);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/*
|
|
* Allocate and initialize frequency table.
|
|
* Last row of frequency table must be set to CPUFREQ_TABLE_END.
|
|
* Table size should be one larger than real table size.
|
|
*/
|
|
domain->freq_table = kzalloc(sizeof(struct cpufreq_frequency_table)
|
|
* (domain->table_size + 1), GFP_KERNEL);
|
|
if (!domain->freq_table) {
|
|
kfree(fv_table);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
for (index = 0; index < domain->table_size; index++) {
|
|
domain->freq_table[index].driver_data = index;
|
|
domain->freq_table[index].frequency = fv_table[index].freq;
|
|
}
|
|
domain->freq_table[index].driver_data = index;
|
|
domain->freq_table[index].frequency = CPUFREQ_TABLE_END;
|
|
|
|
/*
|
|
* Add OPP table for thermal.
|
|
* Thermal CPU cooling is based on the OPP table.
|
|
*/
|
|
for (index = domain->table_size - 1; index >= 0; index--) {
|
|
for_each_cpu_and(cpu, &domain->cpus, cpu_possible_mask)
|
|
dev_pm_opp_add(get_cpu_device(cpu),
|
|
fv_table[index].freq * 1000,
|
|
fv_table[index].volt);
|
|
}
|
|
|
|
kfree(fv_table);
|
|
|
|
/*
|
|
* Initialize other items.
|
|
*/
|
|
domain->resume_freq = cal_dfs_get_resume_freq(domain->cal_id);
|
|
domain->old = get_freq(domain);
|
|
if (domain->old < domain->min_freq || domain->max_freq < domain->old) {
|
|
WARN(1, "Out-of-range freq(%dkhz) returned for domain%d in init time\n",
|
|
domain->old, domain->id);
|
|
domain->old = domain->boot_freq;
|
|
}
|
|
|
|
cur_idx = exynos_cpufreq_find_index(domain, domain->old);
|
|
domain->old = domain->freq_table[cur_idx].frequency;
|
|
|
|
mutex_init(&domain->lock);
|
|
|
|
/*
|
|
* Initialize CPUFreq DVFS Manager
|
|
* DVFS Manager is the optional function, it does not check return value
|
|
*/
|
|
init_dm(domain, dn);
|
|
|
|
/* Register EM to device of CPU in this domain */
|
|
cpu = cpumask_first(&domain->cpus);
|
|
dev_pm_opp_of_register_em(get_cpu_device(cpu), &domain->cpus);
|
|
|
|
/* Initialize fields to test fast switch */
|
|
init_fast_switch(domain, dn);
|
|
|
|
pr_info("Complete to initialize cpufreq-domain%d\n", domain->id);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int acme_cpufreq_policy_notify_callback(struct notifier_block *nb,
|
|
unsigned long val, void *data)
|
|
{
|
|
struct cpufreq_policy *policy = data;
|
|
struct exynos_cpufreq_domain *domain;
|
|
int ret;
|
|
|
|
if (val != CPUFREQ_CREATE_POLICY)
|
|
return NOTIFY_OK;
|
|
|
|
domain = find_domain(policy->cpu);
|
|
if (!domain)
|
|
return NOTIFY_OK;
|
|
|
|
enable_domain(domain);
|
|
|
|
#if IS_ENABLED(CONFIG_EXYNOS_CPU_THERMAL) || IS_ENABLED(CONFIG_EXYNOS_CPU_THERMAL_MODULE)
|
|
exynos_cpufreq_cooling_register(domain->dn, policy);
|
|
#endif
|
|
|
|
ret = init_freq_qos(domain, policy);
|
|
if (ret) {
|
|
pr_info("failed to init pm_qos with err %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
ret = init_fops(domain, policy);
|
|
if (ret) {
|
|
pr_info("failed to init fops with err %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
return NOTIFY_OK;
|
|
}
|
|
|
|
static struct notifier_block acme_cpufreq_policy_nb = {
|
|
.notifier_call = acme_cpufreq_policy_notify_callback,
|
|
.priority = INT_MIN,
|
|
};
|
|
|
|
extern int cpu_dvfs_notifier_register(struct notifier_block *n);
|
|
|
|
#if defined(CONFIG_ARM_EXYNOS_CLDVFS_SYSFS)
|
|
static void init_cldvfs(struct kobject *kobj)
|
|
{
|
|
cldvfs_base = ioremap(CLDVFS_BASE, SZ_4K);
|
|
sysfs_create_group(kobj, &exynos_cldvfs_group);
|
|
}
|
|
#endif
|
|
|
|
static int exynos_cpufreq_probe(struct platform_device *pdev)
|
|
{
|
|
struct device_node *dn;
|
|
struct exynos_cpufreq_domain *domain;
|
|
unsigned int domain_id = 0;
|
|
int ret = 0;
|
|
|
|
/*
|
|
* Pre-initialization.
|
|
*
|
|
* allocate and initialize cpufreq domain
|
|
*/
|
|
for_each_child_of_node(pdev->dev.of_node, dn) {
|
|
domain = kzalloc(sizeof(struct exynos_cpufreq_domain), GFP_KERNEL);
|
|
if (!domain) {
|
|
pr_err("failed to allocate domain%d\n", domain_id);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
domain->id = domain_id++;
|
|
if (init_domain(domain, dn)) {
|
|
pr_err("failed to initialize cpufreq domain%d\n",
|
|
domain->id);
|
|
kfree(domain->freq_table);
|
|
kfree(domain);
|
|
continue;
|
|
}
|
|
|
|
domain->dn = dn;
|
|
list_add_tail(&domain->list, &domains);
|
|
|
|
print_domain_info(domain);
|
|
}
|
|
|
|
if (!domain_id) {
|
|
pr_err("Failed to initialize cpufreq driver\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
cpufreq_register_notifier(&acme_cpufreq_policy_nb, CPUFREQ_POLICY_NOTIFIER);
|
|
|
|
/* Register cpufreq driver */
|
|
ret = cpufreq_register_driver(&exynos_driver);
|
|
if (ret) {
|
|
pr_err("failed to register cpufreq driver\n");
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Post-initialization
|
|
*
|
|
* 1. create sysfs to control frequency min/max
|
|
* 2. enable frequency scaling of each domain
|
|
* 3. initialize freq qos of each domain
|
|
* 4. register notifier bloack
|
|
*/
|
|
init_sysfs(&pdev->dev.kobj);
|
|
|
|
cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN, "exynos:acme",
|
|
exynos_cpufreq_cpu_up_callback, exynos_cpufreq_cpu_down_callback);
|
|
|
|
exynos_dm_fast_switch_notifier_register(&exynos_cpu_fast_switch_nb);
|
|
|
|
emstune_register_notifier(&exynos_cpufreq_mode_update_notifier);
|
|
register_pm_notifier(&exynos_cpufreq_pm);
|
|
|
|
INIT_WORK(&dm_work, change_dm_table_work);
|
|
|
|
pr_info("Initialized Exynos cpufreq driver\n");
|
|
|
|
#if defined(CONFIG_ARM_EXYNOS_CLDVFS_SYSFS)
|
|
init_cldvfs(&pdev->dev.kobj);
|
|
#endif
|
|
|
|
return ret;
|
|
}
|
|
|
|
static const struct of_device_id of_exynos_cpufreq_match[] = {
|
|
{ .compatible = "samsung,exynos-acme", },
|
|
{ },
|
|
};
|
|
MODULE_DEVICE_TABLE(of, of_exynos_cpufreq_match);
|
|
|
|
static struct platform_driver exynos_cpufreq_driver = {
|
|
.driver = {
|
|
.name = "exynos-acme",
|
|
.owner = THIS_MODULE,
|
|
.of_match_table = of_exynos_cpufreq_match,
|
|
},
|
|
.probe = exynos_cpufreq_probe,
|
|
};
|
|
|
|
module_platform_driver(exynos_cpufreq_driver);
|
|
|
|
MODULE_DESCRIPTION("Exynos ACME");
|
|
MODULE_LICENSE("GPL");
|