blob: dff0ff4cc218db12b5dbd9d055cbf2b42fe059a6 [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2014 Imagination Technologies
* Author: Paul Burton <paul.burton@mips.com>
*/
#include <linux/cpu_pm.h>
#include <linux/cpuidle.h>
#include <linux/init.h>
#include <asm/idle.h>
#include <asm/pm-cps.h>
/* Enumeration of the various idle states this driver may enter */
enum cps_idle_state {
STATE_WAIT = 0, /* MIPS wait instruction, coherent */
STATE_NC_WAIT, /* MIPS wait instruction, non-coherent */
STATE_CLOCK_GATED, /* Core clock gated */
STATE_POWER_GATED, /* Core power gated */
STATE_COUNT
};
static int cps_nc_enter(struct cpuidle_device *dev,
struct cpuidle_driver *drv, int index)
{
enum cps_pm_state pm_state;
int err;
/*
* At least one core must remain powered up & clocked in order for the
* system to have any hope of functioning.
*
* TODO: don't treat core 0 specially, just prevent the final core
* TODO: remap interrupt affinity temporarily
*/
if (cpus_are_siblings(0, dev->cpu) && (index > STATE_NC_WAIT))
index = STATE_NC_WAIT;
/* Select the appropriate cps_pm_state */
switch (index) {
case STATE_NC_WAIT:
pm_state = CPS_PM_NC_WAIT;
break;
case STATE_CLOCK_GATED:
pm_state = CPS_PM_CLOCK_GATED;
break;
case STATE_POWER_GATED:
pm_state = CPS_PM_POWER_GATED;
break;
default:
BUG();
return -EINVAL;
}
/* Notify listeners the CPU is about to power down */
if ((pm_state == CPS_PM_POWER_GATED) && cpu_pm_enter())
return -EINTR;
/* Enter that state */
err = cps_pm_enter_state(pm_state);
/* Notify listeners the CPU is back up */
if (pm_state == CPS_PM_POWER_GATED)
cpu_pm_exit();
return err ?: index;
}
static struct cpuidle_driver cps_driver = {
.name = "cpc_cpuidle",
.owner = THIS_MODULE,
.states = {
[STATE_WAIT] = MIPS_CPUIDLE_WAIT_STATE,
[STATE_NC_WAIT] = {
.enter = cps_nc_enter,
.exit_latency = 200,
.target_residency = 450,
.name = "nc-wait",
.desc = "non-coherent MIPS wait",
},
[STATE_CLOCK_GATED] = {
.enter = cps_nc_enter,
.exit_latency = 300,
.target_residency = 700,
.flags = CPUIDLE_FLAG_TIMER_STOP,
.name = "clock-gated",
.desc = "core clock gated",
},
[STATE_POWER_GATED] = {
.enter = cps_nc_enter,
.exit_latency = 600,
.target_residency = 1000,
.flags = CPUIDLE_FLAG_TIMER_STOP,
.name = "power-gated",
.desc = "core power gated",
},
},
.state_count = STATE_COUNT,
.safe_state_index = 0,
};
static void __init cps_cpuidle_unregister(void)
{
int cpu;
struct cpuidle_device *device;
for_each_possible_cpu(cpu) {
device = &per_cpu(cpuidle_dev, cpu);
cpuidle_unregister_device(device);
}
cpuidle_unregister_driver(&cps_driver);
}
static int __init cps_cpuidle_init(void)
{
int err, cpu, i;
struct cpuidle_device *device;
/* Detect supported states */
if (!cps_pm_support_state(CPS_PM_POWER_GATED))
cps_driver.state_count = STATE_CLOCK_GATED + 1;
if (!cps_pm_support_state(CPS_PM_CLOCK_GATED))
cps_driver.state_count = STATE_NC_WAIT + 1;
if (!cps_pm_support_state(CPS_PM_NC_WAIT))
cps_driver.state_count = STATE_WAIT + 1;
/* Inform the user if some states are unavailable */
if (cps_driver.state_count < STATE_COUNT) {
pr_info("cpuidle-cps: limited to ");
switch (cps_driver.state_count - 1) {
case STATE_WAIT:
pr_cont("coherent wait\n");
break;
case STATE_NC_WAIT:
pr_cont("non-coherent wait\n");
break;
case STATE_CLOCK_GATED:
pr_cont("clock gating\n");
break;
}
}
/*
* Set the coupled flag on the appropriate states if this system
* requires it.
*/
if (coupled_coherence)
for (i = STATE_NC_WAIT; i < cps_driver.state_count; i++)
cps_driver.states[i].flags |= CPUIDLE_FLAG_COUPLED;
err = cpuidle_register_driver(&cps_driver);
if (err) {
pr_err("Failed to register CPS cpuidle driver\n");
return err;
}
for_each_possible_cpu(cpu) {
device = &per_cpu(cpuidle_dev, cpu);
device->cpu = cpu;
#ifdef CONFIG_ARCH_NEEDS_CPU_IDLE_COUPLED
cpumask_copy(&device->coupled_cpus, &cpu_sibling_map[cpu]);
#endif
err = cpuidle_register_device(device);
if (err) {
pr_err("Failed to register CPU%d cpuidle device\n",
cpu);
goto err_out;
}
}
return 0;
err_out:
cps_cpuidle_unregister();
return err;
}
device_initcall(cps_cpuidle_init);