blob: c1e5e4c08d6244f0fbc91445e6cf62be78afe684 [file] [log] [blame]
/*
* Copyright (c) 2014 Samsung Electronics Co., Ltd.
* http://www.samsung.com
*
* CPUIDLE driver for exynos 64bit
*
* 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.
*/
#include <linux/cpuidle.h>
#include <linux/cpu_pm.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/suspend.h>
#include <linux/cpu.h>
#include <linux/reboot.h>
#include <linux/of.h>
#include <linux/psci.h>
#include <linux/cpuidle_profiler.h>
#include <linux/exynos-ucc.h>
#include <asm/tlbflush.h>
#include <asm/cpuidle.h>
#include <asm/topology.h>
#include <soc/samsung/exynos-cpupm.h>
#include <linux/cpufreq.h>
#include "dt_idle_states.h"
#define DEFAULT_FREQVAR_IFACTOR 100 /* No more or less for the given constraints */
unsigned int default_freqvar_ifactor[] = {DEFAULT_FREQVAR_IFACTOR};
EXPORT_SYMBOL_GPL(default_freqvar_ifactor);
unsigned int *ank_freqvar_ifactor = default_freqvar_ifactor;
unsigned int *pmt_freqvar_ifactor = default_freqvar_ifactor;
unsigned int *cht_freqvar_ifactor = default_freqvar_ifactor;
DEFINE_PER_CPU(struct freqvariant_idlefactor, fv_ifactor);
DECLARE_PER_CPU(int, clhead_cpu);
/*
* Exynos cpuidle driver supports the below idle states
*
* IDLE_C1 : WFI(Wait For Interrupt) low-power state
* IDLE_C2 : Local CPU power gating
*/
/***************************************************************************
* Cpuidle state handler *
***************************************************************************/
static unsigned int prepare_idle(unsigned int cpu, int index)
{
unsigned int entry_state = 0;
if (index > 0) {
cpu_pm_enter();
entry_state = exynos_cpu_pm_enter(cpu, index);
}
cpuidle_profile_cpu_idle_enter(cpu, index);
return entry_state;
}
static void post_idle(unsigned int cpu, int index, int fail)
{
cpuidle_profile_cpu_idle_exit(cpu, index, fail);
if (!index)
return;
exynos_cpu_pm_exit(cpu, fail);
cpu_pm_exit();
}
static int enter_idle(unsigned int index)
{
/*
* idle state index 0 corresponds to wfi, should never be called
* from the cpu_suspend operations
*/
if (!index) {
cpu_do_idle();
return 0;
}
return arm_cpuidle_suspend(index);
}
DECLARE_PER_CPU(struct cpuidle_info, cpuidle_inf);
static int exynos_enter_idle(struct cpuidle_device *dev,
struct cpuidle_driver *drv, int index)
{
int i, idx, triggered = 0;
int entry_state, ret = 0;
unsigned int bias_target_residency;
unsigned int bias_exit_latency;
unsigned int bias_index;
unsigned long flags;
struct freqvariant_idlefactor *pfv_factor = &per_cpu(fv_ifactor, dev->cpu);
struct cpuidle_info *idle_info = this_cpu_ptr(&cpuidle_inf);
if (idle_info->bUse_GovDecision == 1 || pfv_factor->init != 0xdead)
goto skip_moce;
/* Fetch the criteria for idle state without spinlock
* with the way of Read-Copy-No update
* for lowering the idle transition cost
*/
spin_lock_irqsave(&pfv_factor->freqvar_if_lock, flags);
bias_target_residency = pfv_factor->bias_target_res;
bias_exit_latency = pfv_factor->bias_exit_lat;
bias_index = pfv_factor->bias_index;
spin_unlock_irqrestore(&pfv_factor->freqvar_if_lock, flags);
/* Re-evaluate the depth of idle in accordance with the freq variation */
idx = -1;
for (i = idle_info->bfirst_idx; i < drv->state_count; i++) {
struct cpuidle_state *s = &drv->states[i];
struct cpuidle_state_usage *su = &dev->states_usage[i];
if (s->disabled || su->disable)
continue;
if (idx == -1)
idx = i; /* first enabled state */
/* Future work:
* If the different biasing timing is applied onto plural idle states, pfv_factor also must have
* the different value for each idle state.
*/
if (bias_index & 0x1 << i) {
if (bias_target_residency > idle_info->predicted_us|| bias_exit_latency > idle_info->latency_req)
break;
triggered = 1;
} else if (triggered) {
if (s->target_residency > idle_info->predicted_us || s->exit_latency > idle_info->latency_req)
break;
}
idx = i;
}
/* Update the index /w MOCE */
if (idx != -1 && index != idx) {
index = idx;
}
skip_moce:
entry_state = prepare_idle(dev->cpu, index);
ret = enter_idle(entry_state);
post_idle(dev->cpu, index, ret);
/*
* If cpu fail to enter idle, it should not update state usage
* count. Driver have to return an error value to
* cpuidle_enter_state().
*/
if (ret < 0)
return ret;
else
return index;
}
/***************************************************************************
* Define notifier call *
***************************************************************************/
static int exynos_cpuidle_reboot_notifier(struct notifier_block *this,
unsigned long event, void *_cmd)
{
switch (event) {
case SYSTEM_POWER_OFF:
case SYS_RESTART:
cpuidle_pause();
break;
}
return NOTIFY_OK;
}
static struct notifier_block exynos_cpuidle_reboot_nb = {
.notifier_call = exynos_cpuidle_reboot_notifier,
};
/*
* cpufreq trans call back function :
* freq variant value is kept in only pfv_factor of representing core
* For example, CPU#0 for CPU#0~3, CPU#4 for CPU#4~5
* CPU#6 for CPU#6~7
*/
static int exynos_cpuidle_cpufreq_trans_notifier(struct notifier_block *nb,
unsigned long val, void *data)
{
struct cpufreq_freqs *freq = data;
struct freqvariant_idlefactor *pfv_factor;
unsigned int b_target_res, b_exit_lat, b_cur_freqvar_if;
int leader_cpu, cpu, i;
unsigned long flags;
if (freq->flags & CPUFREQ_CONST_LOOPS)
return NOTIFY_OK;
if (val != CPUFREQ_POSTCHANGE)
return NOTIFY_OK;
leader_cpu = per_cpu(clhead_cpu, freq->cpu);
pfv_factor = &per_cpu(fv_ifactor, leader_cpu);
if (pfv_factor->init != 0xdead)
return NOTIFY_OK;
spin_lock_irqsave(&pfv_factor->freqvar_if_lock, flags);
for (i = 0 ; i < pfv_factor->nfreqvar_ifs - 1 &&
freq->new >= pfv_factor->freqvar_ifs[i+1]; i += 2)
;
pfv_factor->cur_freqvar_if = pfv_factor->freqvar_ifs[i];
b_cur_freqvar_if = pfv_factor->cur_freqvar_if;
b_target_res = (pfv_factor->orig_target_res * pfv_factor->cur_freqvar_if) / 100;
b_exit_lat = (pfv_factor->orig_exit_lat * pfv_factor->cur_freqvar_if) / 100;
if (b_target_res != pfv_factor->bias_target_res ||
b_exit_lat != pfv_factor->bias_exit_lat) {
pfv_factor->bias_target_res = b_target_res;
pfv_factor->bias_exit_lat = b_exit_lat;
spin_unlock_irqrestore(&pfv_factor->freqvar_if_lock, flags);
for_each_possible_cpu(cpu) {
if (per_cpu(clhead_cpu, cpu) == leader_cpu) {
pfv_factor = &per_cpu(fv_ifactor, cpu);
spin_lock_irqsave(&pfv_factor->freqvar_if_lock, flags);
pfv_factor->cur_freqvar_if = b_cur_freqvar_if;
pfv_factor->bias_target_res = b_target_res;
pfv_factor->bias_exit_lat = b_exit_lat;
spin_unlock_irqrestore(&pfv_factor->freqvar_if_lock, flags);
}
}
} else {
spin_unlock_irqrestore(&pfv_factor->freqvar_if_lock, flags);
}
return NOTIFY_OK;
}
static struct notifier_block exynos_cpufreq_trans_nb = {
.notifier_call = exynos_cpuidle_cpufreq_trans_notifier,
};
/***************************************************************************
* Initialize cpuidle driver *
***************************************************************************/
#define exynos_idle_wfi_state(state) \
do { \
state.enter = exynos_enter_idle; \
state.exit_latency = 1; \
state.target_residency = 1; \
state.power_usage = UINT_MAX; \
strncpy(state.name, "WFI", CPUIDLE_NAME_LEN - 1); \
strncpy(state.desc, "c1", CPUIDLE_DESC_LEN - 1); \
} while (0)
static struct cpuidle_driver exynos_idle_driver[NR_CPUS];
static const struct of_device_id exynos_idle_state_match[] __initconst = {
{ .compatible = "exynos,idle-state",
.data = exynos_enter_idle },
{ },
};
static int __init exynos_idle_driver_init(struct cpuidle_driver *drv,
struct cpumask *cpumask)
{
int cpu = cpumask_first(cpumask);
drv->name = kzalloc(sizeof("exynos_idleX"), GFP_KERNEL);
if (!drv->name)
return -ENOMEM;
scnprintf((char *)drv->name, 12, "exynos_idle%d", cpu);
drv->owner = THIS_MODULE;
drv->cpumask = cpumask;
exynos_idle_wfi_state(drv->states[0]);
return 0;
}
static int __init exynos_idle_init(void)
{
struct freqvariant_idlefactor *pfv_factor;
int ret, cpu, i;
for_each_possible_cpu(cpu) {
ret = exynos_idle_driver_init(&exynos_idle_driver[cpu],
topology_sibling_cpumask(cpu));
if (ret) {
pr_err("failed to initialize cpuidle driver for cpu%d",
cpu);
goto out_unregister;
}
/*
* Initialize idle states data, starting at index 1.
* This driver is DT only, if no DT idle states are detected
* (ret == 0) let the driver initialization fail accordingly
* since there is no reason to initialize the idle driver
* if only wfi is supported.
*/
ret = dt_init_idle_driver(&exynos_idle_driver[cpu],
exynos_idle_state_match, 1);
if (ret < 0) {
pr_err("failed to initialize idle state for cpu%d\n", cpu);
goto out_unregister;
}
/*
* Call arch CPU operations in order to initialize
* idle states suspend back-end specific data
*/
ret = arm_cpuidle_init(cpu);
if (ret) {
pr_err("failed to initialize idle operation for cpu%d\n", cpu);
goto out_unregister;
}
ret = cpuidle_register(&exynos_idle_driver[cpu], NULL);
if (ret) {
pr_err("failed to register cpuidle for cpu%d\n", cpu);
goto out_unregister;
}
pfv_factor = &per_cpu(fv_ifactor, cpu);
/* HACK */
switch (cpu) {
case 0 ... 3:
pfv_factor->freqvar_ifs = ank_freqvar_ifactor;
pfv_factor->nfreqvar_ifs = 1;
pfv_factor->cur_freqvar_if = 100;
break;
case 4 ... 5:
pfv_factor->freqvar_ifs = pmt_freqvar_ifactor;
pfv_factor->nfreqvar_ifs = 1;
pfv_factor->cur_freqvar_if = 100;
break;
case 6 ... 7:
pfv_factor->freqvar_ifs = cht_freqvar_ifactor;
pfv_factor->nfreqvar_ifs = 1;
pfv_factor->cur_freqvar_if = 100;
break;
}
/* MOCE will concern only C2. (CPD will take care later) */
pfv_factor->orig_target_res = exynos_idle_driver[cpu].states[1].target_residency;
pfv_factor->orig_exit_lat = exynos_idle_driver[cpu].states[1].exit_latency;
pfv_factor->bias_target_res= exynos_idle_driver[cpu].states[1].target_residency;
pfv_factor->bias_exit_lat= exynos_idle_driver[cpu].states[1].exit_latency;
spin_lock_init(&pfv_factor->freqvar_if_lock);
pfv_factor->bias_index = 0x2; // We only focusing on state index 1 (C2)
pfv_factor->init = 0xdead;
}
cpuidle_profile_cpu_idle_register(&exynos_idle_driver[0]);
register_reboot_notifier(&exynos_cpuidle_reboot_nb);
cpufreq_register_notifier(&exynos_cpufreq_trans_nb,
CPUFREQ_TRANSITION_NOTIFIER);
init_exynos_ucc();
pr_info("Exynos cpuidle driver Initialized\n");
return 0;
out_unregister:
for (i = 0; i <= cpu; i++) {
kfree(exynos_idle_driver[i].name);
/*
* Cpuidle driver of variable "cpu" is always not registered.
* "cpu" should not call cpuidle_unregister().
*/
if (i < cpu)
cpuidle_unregister(&exynos_idle_driver[i]);
}
return ret;
}
device_initcall(exynos_idle_init);