blob: 427334ca32d64450b4ca5f3c2018fb998ceb66bc [file] [log] [blame]
/*
* Copyright (c) 2015 Samsung Electronics Co., Ltd.
*
* Park Bumgyu <bumgyu.park@samsung.com>
*
* CPU Hotplug driver for Exynos
*
* 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/cpu.h>
#include <linux/fb.h>
#include <linux/kthread.h>
#include <linux/pm_qos.h>
#include <linux/suspend.h>
#include <soc/samsung/exynos-cpu_hotplug.h>
static int cpu_hotplug_in(const struct cpumask *mask)
{
int cpu, ret = 0;
for_each_cpu(cpu, mask) {
ret = cpu_up(cpu);
if (ret) {
/*
* -EIO means core fail to come online by itself
* it is critical error
*/
if (ret == -EIO)
panic("I/O error(-EIO) occurs while CPU%d comes online\n", cpu);
/*
* If it fails to enable cpu,
* it cancels cpu hotplug request and retries later.
*/
pr_err("%s: Failed to hotplug in CPU%d with error %d\n",
__func__, cpu, ret);
break;
}
}
return ret;
}
static int cpu_hotplug_out(const struct cpumask *mask)
{
int cpu, ret = 0;
/*
* Reverse order of cpu,
* explore cpu7, cpu6, cpu5, ... cpu1
*/
for (cpu = nr_cpu_ids - 1; cpu > 0; cpu--) {
if (!cpumask_test_cpu(cpu, mask))
continue;
ret = cpu_down(cpu);
if (ret) {
pr_err("%s: Failed to hotplug out CPU%d with error %d\n",
__func__, cpu, ret);
break;
}
}
return ret;
}
static struct {
/* Control cpu hotplug operation */
bool enabled;
/* flag for suspend */
bool suspended;
/* Synchronizes accesses to refcount and cpumask */
struct mutex lock;
/* all CPUs running time during booting */
int boot_lock_time;
/* user input minimum and maximum online cpu */
int user_min;
int user_max;
/*
* In blocking notifier call chain, it is not supposed to call
* cpu_up() or cpu_down(). In this case, use workqueue.
*/
struct workqueue_struct *workqueue;
/*
* During reuesting cpu hotplug by other drivers, cpu hotplug framework
* rejects cpu hotplug request. To guarantee the request, re-request cpu
* hotplug using delayed work.
*/
struct delayed_work delayed_work;
/* cpu_hotplug kobject */
struct kobject *kobj;
} cpu_hotplug = {
.lock = __MUTEX_INITIALIZER(cpu_hotplug.lock),
};
static inline void cpu_hotplug_suspend(bool enable)
{
/* This lock guarantees completion of do_cpu_hotplug() */
mutex_lock(&cpu_hotplug.lock);
cpu_hotplug.suspended = enable;
mutex_unlock(&cpu_hotplug.lock);
}
static inline void update_enable_flag(bool enable)
{
mutex_lock(&cpu_hotplug.lock);
cpu_hotplug.enabled = enable;
mutex_unlock(&cpu_hotplug.lock);
}
struct kobject *exynos_cpu_hotplug_kobj(void)
{
return cpu_hotplug.kobj;
}
bool exynos_cpu_hotplug_enabled(void)
{
return cpu_hotplug.enabled;
}
/*
* If somebody requests CPU hotplug, hotplug driver creates cpumask with minimum
* and maxinum online CPU in PM QoS. The number of online CPU will be same as
* minimum online CPU on the assumption that minimum online CPU is not greater
* than maximum online CPU. If mininum is greater than maximum, online CPU will
* be maximum.
*/
static struct cpumask create_cpumask(void)
{
int online_cpu_min, online_cpu_max;
int cpu;
struct cpumask mask;
online_cpu_min = min(pm_qos_request(PM_QOS_CPU_ONLINE_MIN), nr_cpu_ids);
online_cpu_max = min(pm_qos_request(PM_QOS_CPU_ONLINE_MAX), nr_cpu_ids);
cpumask_clear(&mask);
for_each_possible_cpu(cpu) {
if (!cpumask_test_cpu(cpu, &early_cpu_mask))
continue;
if (cpumask_weight(&mask) < online_cpu_min)
cpumask_set_cpu(cpu, &mask);
else
break;
}
for (cpu = num_possible_cpus()-1; cpu >= online_cpu_max; cpu--) {
if (!cpumask_test_cpu(cpu, &early_cpu_mask))
continue;
cpumask_clear_cpu(cpu, &mask);
}
return mask;
}
/*
* do_cpu_hotplug() is the main function for cpu hotplug. Only this function
* enables or disables cpus, so all APIs in this driver call do_cpu_hotplug()
* eventually.
*/
static int do_cpu_hotplug(bool fast_hotplug)
{
int ret = 0;
struct cpumask disable_cpus, enable_cpus;
char cpus_buf[10];
int (*func_cpu_down)(const struct cpumask *);
int (*func_cpu_up)(const struct cpumask *);
mutex_lock(&cpu_hotplug.lock);
/*
* If cpu hotplug is disabled or suspended,
* do_cpu_hotplug() do nothing.
*/
if (!cpu_hotplug.enabled || cpu_hotplug.suspended) {
mutex_unlock(&cpu_hotplug.lock);
return 0;
}
/* Create online cpumask */
enable_cpus = create_cpumask();
/*
* disable_cpus is the opposite of enable_cpus:
* disable_cpus = ~enable_cpus
*/
cpumask_xor(&disable_cpus, cpu_possible_mask, &enable_cpus);
/*
* Remove unnecessary cpumask bit:
* enable_cpus = enable_cpus & ~online mask
* disable_cpus = disable_cpus & online mask
*/
cpumask_andnot(&enable_cpus, &enable_cpus, cpu_online_mask);
cpumask_and(&disable_cpus, &disable_cpus, cpu_online_mask);
#ifdef CONFIG_SCHED_HMP
/*
* HACK: fast-hotplug does not support disabling all big cpus.
*/
if (fast_hotplug) {
struct cpumask temp;
cpumask_and(&temp, &hmp_fast_cpu_mask, cpu_online_mask);
cpumask_andnot(&temp, &temp, &disable_cpus);
if (cpumask_empty(&temp)) {
mutex_unlock(&cpu_hotplug.lock);
return 0;
}
}
#endif
scnprintf(cpus_buf, sizeof(cpus_buf), "%*pbl", cpumask_pr_args(&enable_cpus));
pr_debug("%s: enable_cpus=%s\n", __func__, cpus_buf);
scnprintf(cpus_buf, sizeof(cpus_buf), "%*pbl", cpumask_pr_args(&disable_cpus));
pr_debug("%s: disable_cpus=%s\n", __func__, cpus_buf);
/* select function of cpu hotplug */
if (fast_hotplug) {
func_cpu_up = cpus_up;
func_cpu_down = cpus_down;
} else {
func_cpu_up = cpu_hotplug_in;
func_cpu_down = cpu_hotplug_out;
}
if (!cpumask_empty(&enable_cpus)) {
ret = func_cpu_up(&enable_cpus);
if (ret)
goto out;
}
if (!cpumask_empty(&disable_cpus))
ret = func_cpu_down(&disable_cpus);
out:
/* If it fails to complete cpu hotplug request, retries after 100ms */
if (ret)
queue_delayed_work(cpu_hotplug.workqueue, &cpu_hotplug.delayed_work,
msecs_to_jiffies(100));
mutex_unlock(&cpu_hotplug.lock);
return ret;
}
static void cpu_hotplug_work(struct work_struct *work)
{
do_cpu_hotplug(false);
}
static int control_cpu_hotplug(bool enable)
{
struct cpumask mask;
int ret = 0;
if (enable) {
update_enable_flag(true);
do_cpu_hotplug(false);
} else {
mutex_lock(&cpu_hotplug.lock);
cpumask_setall(&mask);
cpumask_andnot(&mask, &mask, cpu_online_mask);
cpumask_and(&mask, &mask, &early_cpu_mask);
/*
* If it success to enable all CPUs, clear cpu_hotplug.enabled flag.
* Since then all hotplug requests are ignored.
*/
ret = cpu_hotplug_in(&mask);
if (!ret) {
/*
* In this position, can't use update_enable_flag()
* because already taken cpu_hotplug.lock
*/
cpu_hotplug.enabled = false;
} else {
pr_err("Fail to disable cpu hotplug, please try again\n");
}
mutex_unlock(&cpu_hotplug.lock);
}
return ret;
}
/*
* If PM_QOS_CPU_ONLINE_MIN and PM_QOS_CPU_ONLINE_MAX request is updated,
* cpu_hotplug_qos_handler is called.
*/
static int cpu_hotplug_qos_handler(struct notifier_block *b,
unsigned long val, void *v)
{
long *p = (long *)v;
/* use fast cpu hotplug sequence */
if (p && *p == FAST_HP)
return do_cpu_hotplug(true);
return do_cpu_hotplug(false);
}
static struct notifier_block cpu_hotplug_qos_notifier = {
.notifier_call = cpu_hotplug_qos_handler,
};
/*
* Requests to control minimum/maximum online cpu
*/
struct pm_qos_request user_min_cpu_hotplug_request;
struct pm_qos_request user_max_cpu_hotplug_request;
/*
* User can change the number of online cpu by using min_online_cpu and
* max_online_cpu sysfs node. User input minimum and maxinum online cpu
* to this node as below:
*
* #echo min > /sys/power/cpuhotplug/min_online_cpu
* #echo max > /sys/power/cpuhotplug/max_online_cpus
*/
#define attr_online_cpu(type) \
static ssize_t show_##type##_online_cpu(struct kobject *kobj, \
struct kobj_attribute *attr, char *buf) \
{ \
return snprintf(buf, 30, #type " online cpu : %d\n", \
cpu_hotplug.user_##type); \
} \
\
static ssize_t store_##type##_online_cpu(struct kobject *kobj, \
struct kobj_attribute *attr, const char *buf, \
size_t count) \
{ \
int input; \
\
if (!sscanf(buf, "%d", &input)) \
return -EINVAL; \
\
if (input <= 0 || input > NR_CPUS) \
return -EINVAL; \
\
pm_qos_update_request(&user_##type##_cpu_hotplug_request, \
input); \
cpu_hotplug.user_##type = input; \
\
return count; \
} \
\
static struct kobj_attribute type##_online_cpu = \
__ATTR(type##_online_cpu, 0644, \
show_##type##_online_cpu, store_##type##_online_cpu)
attr_online_cpu(min);
attr_online_cpu(max);
/*
* User can control the cpu hotplug operation as below:
*
* #echo 1 > /sys/power/cpuhotplug/enabled => enable
* #echo 0 > /sys/power/cpuhotplug/enabled => disable
*
* If enabled become 0, hotplug driver enable the all cpus and no hotplug
* operation happen from hotplug driver.
*/
static ssize_t show_cpu_hotplug_enable(struct kobject *kobj,
struct kobj_attribute *attr, char *buf)
{
return snprintf(buf, 10, "%d\n", cpu_hotplug.enabled);
}
static ssize_t store_cpu_hotplug_enable(struct kobject *kobj,
struct kobj_attribute *attr, const char *buf,
size_t count)
{
int input;
if (!sscanf(buf, "%d", &input))
return -EINVAL;
control_cpu_hotplug(!!input);
return count;
}
static struct kobj_attribute cpu_hotplug_enabled =
__ATTR(enabled, 0644, show_cpu_hotplug_enable, store_cpu_hotplug_enable);
static struct attribute *cpu_hotplug_attrs[] = {
&min_online_cpu.attr,
&max_online_cpu.attr,
&cpu_hotplug_enabled.attr,
NULL,
};
static const struct attribute_group cpu_hotplug_group = {
.attrs = cpu_hotplug_attrs,
};
static void __init cpu_hotplug_dt_init(void)
{
struct device_node *np = of_find_node_by_name(NULL, "cpu_hotplug");
if (of_property_read_u32(np, "boot_lock_time", &cpu_hotplug.boot_lock_time)) {
cpu_hotplug.boot_lock_time = 40;
pr_warn("boot_lock_time property is omitted!\n");
return;
}
}
static int exynos_cpu_hotplug_pm_notifier(struct notifier_block *notifier,
unsigned long pm_event, void *v)
{
switch (pm_event) {
case PM_SUSPEND_PREPARE:
cpu_hotplug_suspend(true);
break;
case PM_POST_SUSPEND:
cpu_hotplug_suspend(false);
do_cpu_hotplug(false);
break;
}
return NOTIFY_OK;
}
static struct notifier_block exynos_cpu_hotplug_nb = {
.notifier_call = exynos_cpu_hotplug_pm_notifier,
};
static struct pm_qos_request boot_min_cpu_hotplug_request;
static void __init cpu_hotplug_pm_qos_init(void)
{
unsigned int default_min = cpumask_weight(&early_cpu_mask);
/* Register PM QoS notifier handler */
pm_qos_add_notifier(PM_QOS_CPU_ONLINE_MIN, &cpu_hotplug_qos_notifier);
pm_qos_add_notifier(PM_QOS_CPU_ONLINE_MAX, &cpu_hotplug_qos_notifier);
/* Guarantee all CPUs running during booting time */
pm_qos_add_request(&boot_min_cpu_hotplug_request,
PM_QOS_CPU_ONLINE_MIN, default_min);
#ifdef CONFIG_EXYNOS_HOTPLUG_GOVERNOR
/*
* If hotplug governor is not activated, nobody may be requested
* PM_QOS_CPU_ONLINE_MIN, all secondary CPUs can go out. To prevent
* this, it updates QoS to NR_CPUS.
*/
pm_qos_update_request_timeout(&boot_min_cpu_hotplug_request,
default_min, cpu_hotplug.boot_lock_time * USEC_PER_SEC);
#endif
/* Add PM QoS for sysfs node */
pm_qos_add_request(&user_min_cpu_hotplug_request,
PM_QOS_CPU_ONLINE_MIN, PM_QOS_CPU_ONLINE_MIN_DEFAULT_VALUE);
pm_qos_add_request(&user_max_cpu_hotplug_request,
PM_QOS_CPU_ONLINE_MAX, PM_QOS_CPU_ONLINE_MAX_DEFAULT_VALUE);
}
static void __init cpu_hotplug_sysfs_init(void)
{
cpu_hotplug.kobj = kobject_create_and_add("cpuhotplug", power_kobj);
if (!cpu_hotplug.kobj) {
pr_err("Fail to create cpu_hotplug kboject\n");
return;
}
/* Create /sys/power/cpuhotplug */
if (sysfs_create_group(cpu_hotplug.kobj, &cpu_hotplug_group)) {
pr_err("Fail to create cpu_hotplug group\n");
return;
}
/* link cpuhotplug directory to /sys/devices/system/cpu/cpuhotplug */
if (sysfs_create_link(&cpu_subsys.dev_root->kobj, cpu_hotplug.kobj, "cpuhotplug"))
pr_err("Fail to link cpuhotplug directory");
}
static int __init cpu_hotplug_init(void)
{
/* Initialize delayed work */
INIT_DELAYED_WORK(&cpu_hotplug.delayed_work, cpu_hotplug_work);
/* Initialize workqueue */
cpu_hotplug.workqueue = alloc_workqueue("%s", WQ_HIGHPRI | WQ_UNBOUND |\
WQ_MEM_RECLAIM | WQ_FREEZABLE,
1, "exynos_cpu_hotplug");
if (!cpu_hotplug.workqueue)
return -ENOMEM;
/* Parse data from device tree */
cpu_hotplug_dt_init();
/* Initialize pm_qos request and handler */
cpu_hotplug_pm_qos_init();
/* Create sysfs */
cpu_hotplug_sysfs_init();
/* register pm notifier */
register_pm_notifier(&exynos_cpu_hotplug_nb);
/* Enable cpu_hotplug */
update_enable_flag(true);
return 0;
}
arch_initcall(cpu_hotplug_init);