615 lines
14 KiB
C
Executable file
615 lines
14 KiB
C
Executable file
/*
|
|
* Copyright (c) 2018 Samsung Electronics Co., Ltd.
|
|
*
|
|
* CPU Part
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
|
|
|
#include <linux/cpu.h>
|
|
#include <linux/fb.h>
|
|
#include <linux/kthread.h>
|
|
#include <linux/pm_qos.h>
|
|
#include <linux/suspend.h>
|
|
#include <linux/module.h>
|
|
#include <linux/moduleparam.h>
|
|
#include <linux/platform_device.h>
|
|
|
|
#include <soc/samsung/debug-snapshot.h>
|
|
#include <soc/samsung/exynos-cpuhp.h>
|
|
|
|
static struct {
|
|
/* Control cpu hotplug operation */
|
|
bool enabled;
|
|
|
|
/* flag for suspend */
|
|
bool suspended;
|
|
|
|
/* flag for debug print */
|
|
bool debug;
|
|
|
|
/* list head for requester */
|
|
struct list_head req_list;
|
|
|
|
/* default cpuhp request */
|
|
struct cpuhp_request default_req;
|
|
|
|
/* cpuhp request for sysfs */
|
|
struct cpuhp_request sysfs_req;
|
|
|
|
/* cpuhp control cpus */
|
|
struct cpumask available_mask;
|
|
|
|
/* Synchronizes accesses to refcount and cpumask */
|
|
struct mutex lock;
|
|
|
|
/* requested cpuhp mask */
|
|
struct cpumask mask;
|
|
|
|
/* cpuhp kobject */
|
|
struct kobject *kobj;
|
|
} cpuhp = {
|
|
.lock = __MUTEX_INITIALIZER(cpuhp.lock),
|
|
};
|
|
|
|
static char *available_cpus;
|
|
module_param(available_cpus, charp, 0);
|
|
|
|
static char *setup_cpus;
|
|
module_param(setup_cpus, charp, 0);
|
|
/******************************************************************************/
|
|
/* Helper functions */
|
|
/******************************************************************************/
|
|
static int cpuhp_do(void);
|
|
|
|
/*
|
|
* Update pm_suspend status.
|
|
* During suspend-resume, cpuhp driver is stop
|
|
*/
|
|
static inline void cpuhp_suspend(bool enable)
|
|
{
|
|
/* This lock guarantees completion of cpuhp_do() */
|
|
mutex_lock(&cpuhp.lock);
|
|
cpuhp.suspended = enable;
|
|
mutex_unlock(&cpuhp.lock);
|
|
}
|
|
|
|
/*
|
|
* Update cpuhp enablestatus.
|
|
* cpuhp driver is working when enabled big is TRUE
|
|
*/
|
|
static inline void cpuhp_enable(bool enable)
|
|
{
|
|
mutex_lock(&cpuhp.lock);
|
|
cpuhp.enabled = enable;
|
|
mutex_unlock(&cpuhp.lock);
|
|
}
|
|
|
|
/******************************************************************************/
|
|
/* External APIs */
|
|
/******************************************************************************/
|
|
int exynos_cpuhp_remove_request(struct cpuhp_request *req)
|
|
{
|
|
mutex_lock(&cpuhp.lock);
|
|
|
|
req->active = 0;
|
|
list_del(&req->list);
|
|
|
|
mutex_unlock(&cpuhp.lock);
|
|
|
|
return cpuhp_do();
|
|
}
|
|
EXPORT_SYMBOL_GPL(exynos_cpuhp_remove_request);
|
|
|
|
int exynos_cpuhp_add_request(struct cpuhp_request *req)
|
|
{
|
|
mutex_lock(&cpuhp.lock);
|
|
|
|
if (req->active) {
|
|
pr_info("cpuhp request(%s) is already added\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
list_add(&req->list, &cpuhp.req_list);
|
|
req->active = 1;
|
|
|
|
cpumask_clear(&req->mask);
|
|
pr_info("Register new cpuhp request (name=%s)\n", req->name);
|
|
|
|
mutex_unlock(&cpuhp.lock);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(exynos_cpuhp_add_request);
|
|
|
|
int exynos_cpuhp_update_request(struct cpuhp_request *req,
|
|
const struct cpumask *mask)
|
|
{
|
|
cpumask_copy(&req->mask, mask);
|
|
|
|
return cpuhp_do();
|
|
}
|
|
EXPORT_SYMBOL_GPL(exynos_cpuhp_update_request);
|
|
|
|
/******************************************************************************/
|
|
/* CPUHP handlers */
|
|
/******************************************************************************/
|
|
/* legacy hotplug in */
|
|
static int cpuhp_in(const struct cpumask *mask)
|
|
{
|
|
int cpu, ret = 0;
|
|
|
|
for_each_cpu(cpu, mask) {
|
|
ret = add_cpu(cpu);
|
|
if (ret < 0) {
|
|
/*
|
|
* If it fails to enable cpu,
|
|
* it cancels cpu hotplug request and retries later.
|
|
*/
|
|
pr_err("Failed to hotplug in CPU%d with error %d\n",
|
|
cpu, ret);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return ret < 0 ? ret : 0;
|
|
}
|
|
|
|
/* legacy hotplug out */
|
|
static int cpuhp_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 = remove_cpu(cpu);
|
|
if (ret < 0) {
|
|
pr_err("Failed to hotplug out CPU%d with error %d\n",
|
|
cpu, ret);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return ret < 0 ? ret : 0;
|
|
}
|
|
|
|
static struct cpumask cpuhp_get_new_mask(void)
|
|
{
|
|
struct cpumask mask;
|
|
struct cpuhp_request *req;
|
|
|
|
cpumask_copy(&mask, &cpuhp.available_mask);
|
|
|
|
list_for_each_entry(req, &cpuhp.req_list, list)
|
|
cpumask_and(&mask, &mask, &req->mask);
|
|
|
|
if (cpumask_empty(&mask))
|
|
pr_warn("Requsted cpuhp mask is empty\n");
|
|
|
|
return mask;
|
|
}
|
|
|
|
/*
|
|
* Executes cpu_up
|
|
*/
|
|
static int cpuhp_cpu_up(struct cpumask *enable_cpus)
|
|
{
|
|
if (cpumask_empty(enable_cpus))
|
|
return 0;
|
|
|
|
return cpuhp_in(enable_cpus);
|
|
}
|
|
|
|
/*
|
|
* Executes cpu_down
|
|
*/
|
|
static int cpuhp_cpu_down(struct cpumask *disable_cpus)
|
|
{
|
|
if (cpumask_empty(disable_cpus))
|
|
return 0;
|
|
|
|
return cpuhp_out(disable_cpus);
|
|
}
|
|
|
|
/* print cpu control informatoin for deubgging */
|
|
static void cpuhp_print_debug_info(struct cpumask *new_mask)
|
|
{
|
|
char new_buf[10], cur_buf[10];
|
|
|
|
scnprintf(cur_buf, sizeof(cur_buf), "%*pbl", cpumask_pr_args(&cpuhp.mask));
|
|
scnprintf(new_buf, sizeof(new_buf), "%*pbl", cpumask_pr_args(new_mask));
|
|
dbg_snapshot_printk("%s: %s -> %s\n", __func__, cur_buf, new_buf);
|
|
|
|
/* print cpu control information */
|
|
if (cpuhp.debug)
|
|
pr_info("%s: %s -> %s\n", __func__, cur_buf, new_buf);
|
|
}
|
|
|
|
static int __cpuhp_do(struct cpumask *req_mask)
|
|
{
|
|
struct cpumask incoming_cpus, outgoing_cpus;
|
|
int ret = 0;
|
|
|
|
/* get the new online cpus mask */
|
|
cpumask_andnot(&incoming_cpus, req_mask, &cpuhp.mask);
|
|
/* get the new offline cpus mask */
|
|
cpumask_andnot(&outgoing_cpus, &cpuhp.mask, req_mask);
|
|
|
|
if (!cpumask_empty(&incoming_cpus))
|
|
ret = cpuhp_cpu_up(&incoming_cpus);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (!cpumask_empty(&outgoing_cpus))
|
|
ret = cpuhp_cpu_down(&outgoing_cpus);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* cpuhp_do() is the main function for cpu hotplug. Only this function
|
|
* enables or disables cpus, so all APIs in this driver call cpuhp_do()
|
|
* eventually.
|
|
*/
|
|
static int cpuhp_do(void)
|
|
{
|
|
struct cpumask new_mask;
|
|
int ret = 0;
|
|
|
|
mutex_lock(&cpuhp.lock);
|
|
/*
|
|
* If cpu hotplug is disabled or suspended,
|
|
* cpuhp_do() do nothing.
|
|
*/
|
|
if (!cpuhp.enabled || cpuhp.suspended) {
|
|
mutex_unlock(&cpuhp.lock);
|
|
return 0;
|
|
}
|
|
|
|
new_mask = cpuhp_get_new_mask();
|
|
cpuhp_print_debug_info(&new_mask);
|
|
|
|
/* if there is no mask change, skip */
|
|
if (cpumask_empty(&new_mask) ||
|
|
cpumask_equal(&cpuhp.mask, &new_mask))
|
|
goto out;
|
|
|
|
ret = __cpuhp_do(&new_mask);
|
|
if (ret)
|
|
goto out;
|
|
|
|
cpumask_and(&cpuhp.mask, &new_mask, cpu_online_mask);
|
|
|
|
out:
|
|
mutex_unlock(&cpuhp.lock);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int cpuhp_control(bool enable)
|
|
{
|
|
struct cpumask mask;
|
|
int ret = 0;
|
|
|
|
if (enable) {
|
|
cpuhp_enable(true);
|
|
cpuhp_do();
|
|
} else {
|
|
mutex_lock(&cpuhp.lock);
|
|
|
|
cpumask_copy(&mask, cpu_possible_mask);
|
|
cpumask_andnot(&mask, &mask, cpu_online_mask);
|
|
|
|
/*
|
|
* If it success to enable all CPUs, clear cpuhp.enabled flag.
|
|
* Since then all hotplug requests are ignored.
|
|
*/
|
|
ret = cpuhp_in(&mask);
|
|
if (!ret) {
|
|
/*
|
|
* In this position, can't use cpuhp_enable()
|
|
* because already taken cpuhp.lock
|
|
*/
|
|
cpuhp.enabled = false;
|
|
} else {
|
|
pr_err("Failed to disable cpu hotplug, please try again\n");
|
|
}
|
|
|
|
mutex_unlock(&cpuhp.lock);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
/* SYSFS Interface */
|
|
/******************************************************************************/
|
|
/*
|
|
* 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 mask > /sys/power/cpuhp/set_online_cpu
|
|
*/
|
|
#define STR_LEN 6
|
|
static ssize_t set_online_cpu_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
return snprintf(buf, 30, "set online cpu : 0x%x\n",
|
|
*(unsigned int *)cpumask_bits(&cpuhp.sysfs_req.mask));
|
|
}
|
|
|
|
static ssize_t set_online_cpu_store(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
struct cpumask online_cpus;
|
|
char str[STR_LEN], re_str[STR_LEN];
|
|
unsigned int cpumask_value;
|
|
|
|
if (strlen(buf) >= STR_LEN)
|
|
return -EINVAL;
|
|
|
|
if (!sscanf(buf, "%5s", str))
|
|
return -EINVAL;
|
|
|
|
if (str[0] == '0' && toupper(str[1]) == 'X')
|
|
/* Move str pointer to remove "0x" */
|
|
cpumask_parse(str + 2, &online_cpus);
|
|
else {
|
|
if (!sscanf(str, "%d", &cpumask_value))
|
|
return -EINVAL;
|
|
|
|
snprintf(re_str, STR_LEN - 1, "%x", cpumask_value);
|
|
cpumask_parse(re_str, &online_cpus);
|
|
}
|
|
|
|
if (!cpumask_test_cpu(0, &online_cpus)) {
|
|
pr_warn("wrong format\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
exynos_cpuhp_update_request(&cpuhp.sysfs_req, &online_cpus);
|
|
|
|
return count;
|
|
}
|
|
DEVICE_ATTR_RW(set_online_cpu);
|
|
|
|
/*
|
|
* It shows cpuhp request information(name, requesting cpu_mask)
|
|
* registered in cpuhp req list
|
|
*
|
|
* #cat /sys/power/cpuhp/reqs
|
|
*/
|
|
static ssize_t reqs_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct cpuhp_request *req;
|
|
ssize_t ret = 0;
|
|
|
|
list_for_each_entry(req, &cpuhp.req_list, list)
|
|
ret += scnprintf(&buf[ret], 30, "%-16s: (0x%x)\n",
|
|
req->name, *(unsigned int *)cpumask_bits(&req->mask));
|
|
|
|
ret += snprintf(&buf[ret], 30, "available cpu : 0x%x\n",
|
|
*(unsigned int *)cpumask_bits(&cpuhp.available_mask));
|
|
ret += snprintf(&buf[ret], 30, "\nrequested cpu: 0x%x\n",
|
|
*(unsigned int *)cpumask_bits(&cpuhp.mask));
|
|
|
|
return ret;
|
|
}
|
|
DEVICE_ATTR_RO(reqs);
|
|
|
|
/*
|
|
* User can control the cpu hotplug operation as below:
|
|
*
|
|
* #echo 1 > /sys/power/cpuhp/enabled => enable
|
|
* #echo 0 > /sys/power/cpuhp/enabled => disable
|
|
*
|
|
* If enabled become 0, hotplug driver enable the all cpus and no hotplug
|
|
* operation happen from hotplug driver.
|
|
*/
|
|
static ssize_t enabled_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
return snprintf(buf, 10, "%d\n", cpuhp.enabled);
|
|
}
|
|
|
|
static ssize_t enabled_store(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
int input;
|
|
|
|
if (!sscanf(buf, "%d", &input))
|
|
return -EINVAL;
|
|
|
|
cpuhp_control(!!input);
|
|
|
|
return count;
|
|
}
|
|
DEVICE_ATTR_RW(enabled);
|
|
|
|
/*
|
|
* User can control en/disable debug mode
|
|
*
|
|
* #echo 1 > /sys/power/cpuhp/debug => enable
|
|
* #echo 0 > /sys/power/cpuhp/debug => disable
|
|
*
|
|
* When it is enabled, information is printed every time there is a cpu control
|
|
*/
|
|
static ssize_t debug_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
return snprintf(buf, 10, "%d\n", cpuhp.debug);
|
|
}
|
|
|
|
static ssize_t debug_store(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
int input;
|
|
|
|
if (!sscanf(buf, "%d", &input))
|
|
return -EINVAL;
|
|
|
|
cpuhp.debug = !!input;
|
|
|
|
return count;
|
|
}
|
|
DEVICE_ATTR_RW(debug);
|
|
|
|
static struct attribute *exynos_cpuhp_attrs[] = {
|
|
&dev_attr_set_online_cpu.attr,
|
|
&dev_attr_reqs.attr,
|
|
&dev_attr_enabled.attr,
|
|
&dev_attr_debug.attr,
|
|
NULL,
|
|
};
|
|
|
|
static struct attribute_group exynos_cpuhp_group = {
|
|
.name = "cpuhp",
|
|
.attrs = exynos_cpuhp_attrs,
|
|
};
|
|
|
|
/******************************************************************************/
|
|
/* Initialize Driver */
|
|
/******************************************************************************/
|
|
static int parse_cpumask(char *str_cpus, struct cpumask *dest_mask)
|
|
{
|
|
if (str_cpus[0] == '0' && toupper(str_cpus[1]) == 'X')
|
|
/* Move str pointer to remove "0x" */
|
|
cpumask_parse(str_cpus + 2, dest_mask);
|
|
else
|
|
cpumask_parse(str_cpus, dest_mask);
|
|
|
|
cpumask_and(dest_mask, dest_mask, cpu_possible_mask);
|
|
cpumask_and(dest_mask, dest_mask, &cpuhp.available_mask);
|
|
|
|
if (!cpumask_test_cpu(0, dest_mask)) {
|
|
pr_warn("wrong format\n");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void init_setup_cpus(void)
|
|
{
|
|
struct cpumask setup_mask;
|
|
int ret;
|
|
|
|
ret = parse_cpumask(setup_cpus, &setup_mask);
|
|
if (ret)
|
|
return;
|
|
|
|
mutex_lock(&cpuhp.lock);
|
|
ret = __cpuhp_do(&setup_mask);
|
|
mutex_unlock(&cpuhp.lock);
|
|
|
|
if (ret)
|
|
pr_warn("%s: Failed setup_cpus %*pbl", cpumask_pr_args(&setup_mask));
|
|
else
|
|
pr_info("%s: bootargs setup_cpus = %s\n", __func__, setup_cpus);
|
|
}
|
|
|
|
static void init_available_cpus(void)
|
|
{
|
|
struct cpumask mask;
|
|
int ret;
|
|
|
|
cpumask_copy(&cpuhp.available_mask, cpu_possible_mask);
|
|
|
|
if (!available_cpus)
|
|
return;
|
|
|
|
ret = parse_cpumask(available_cpus, &mask);
|
|
if (ret)
|
|
return;
|
|
|
|
cpumask_copy(&cpuhp.available_mask, &mask);
|
|
|
|
pr_info("%s: bootargs available_cpus = %s\n", __func__, available_cpus);
|
|
}
|
|
|
|
static void cpuhp_init(void)
|
|
{
|
|
/* init req list */
|
|
INIT_LIST_HEAD(&cpuhp.req_list);
|
|
|
|
/* register default cpuhp request */
|
|
strcpy(cpuhp.default_req.name, "cpuhp_default");
|
|
exynos_cpuhp_add_request(&cpuhp.default_req);
|
|
exynos_cpuhp_update_request(&cpuhp.default_req, cpu_possible_mask);
|
|
|
|
/* register cpuhp request for sysfs */
|
|
strcpy(cpuhp.sysfs_req.name, "cpuhp_sysfs");
|
|
exynos_cpuhp_add_request(&cpuhp.sysfs_req);
|
|
exynos_cpuhp_update_request(&cpuhp.sysfs_req, cpu_possible_mask);
|
|
|
|
cpumask_copy(&cpuhp.mask, cpu_online_mask);
|
|
|
|
init_available_cpus();
|
|
|
|
cpuhp_enable(true);
|
|
|
|
if (setup_cpus)
|
|
init_setup_cpus();
|
|
}
|
|
|
|
static int exynos_cpuhp_probe(struct platform_device *pdev)
|
|
{
|
|
/* Create CPUHP sysfs */
|
|
if (sysfs_create_group(&pdev->dev.kobj, &exynos_cpuhp_group))
|
|
pr_err("Failed to create sysfs for CPUHP\n");
|
|
|
|
/* Link CPUHP sysfs to /sys/devices/system/cpu/cpuhp */
|
|
if (sysfs_create_link(&cpu_subsys.dev_root->kobj,
|
|
&pdev->dev.kobj, "cpuhp"))
|
|
pr_err("Failed to link CPUHP sysfs to cpuctrl\n");
|
|
|
|
cpuhp_init();
|
|
|
|
pr_info("Exynos CPUHP driver probe done!\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct of_device_id of_exynos_cpuhp_match[] = {
|
|
{ .compatible = "samsung,exynos-cpuhp", },
|
|
{ },
|
|
};
|
|
MODULE_DEVICE_TABLE(of, of_exynos_cpuhp_match);
|
|
|
|
static struct platform_driver exynos_cpuhp_driver = {
|
|
.driver = {
|
|
.name = "exynos-cpuhp",
|
|
.owner = THIS_MODULE,
|
|
.of_match_table = of_exynos_cpuhp_match,
|
|
},
|
|
.probe = exynos_cpuhp_probe,
|
|
};
|
|
|
|
static int __init exynos_cpuhp_init(void)
|
|
{
|
|
return platform_driver_register(&exynos_cpuhp_driver);
|
|
}
|
|
arch_initcall(exynos_cpuhp_init);
|
|
|
|
static void __exit exynos_cpuhp_exit(void)
|
|
{
|
|
platform_driver_unregister(&exynos_cpuhp_driver);
|
|
}
|
|
module_exit(exynos_cpuhp_exit);
|
|
|
|
MODULE_DESCRIPTION("Exynos CPUHP driver");
|
|
MODULE_LICENSE("GPL");
|