kernel_samsung_a53x/drivers/soc/samsung/exynos-ufcc.c
2024-10-17 12:50:20 -03:00

1709 lines
40 KiB
C
Executable file

/*
* 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.
*
* User Frequency & Cstate Control driver
*
* Copyright (C) 2020 Samsung Electronics Co., Ltd
* Author : Park Bumgyu <bumgyu.park@samsung.com>
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/cpumask.h>
#include <linux/of.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/init.h>
#include <linux/cpu.h>
#include <linux/cpufreq.h>
#include <linux/pm_opp.h>
#include <linux/ems.h>
#include <soc/samsung/exynos-cpupm.h>
#include <soc/samsung/exynos-ufcc.h>
#include <soc/samsung/freq-qos-tracer.h>
/*********************************************************************
* USER CSTATE CONTROL *
*********************************************************************/
static int ucc_initialized = false;
/*
* struct ucc_config
*
* - index : index of cstate config
* - cstate : mask indicating whether each cpu supports cstate
* - mask bit set : cstate enable
* - mask bit unset : cstate disable
*/
struct ucc_config {
int index;
struct cpumask cstate;
};
static struct ucc_config *ucc_configs;
static int ucc_config_count;
/*
* cur_cstate has cpu cstate config corresponding to maximum index among ucc
* request. Whenever maximum requested index is changed, cur_state is updated.
*/
static struct cpumask cur_cstate;
static LIST_HEAD(ucc_req_list);
static DEFINE_SPINLOCK(ucc_lock);
static int ucc_get_value(void)
{
struct ucc_req *req;
req = list_first_entry_or_null(&ucc_req_list, struct ucc_req, list);
return req ? req->value : -1;
}
static void update_cur_cstate(void)
{
int value = ucc_get_value();
if (value < 0) {
cpumask_copy(&cur_cstate, cpu_possible_mask);
return;
}
cpumask_copy(&cur_cstate, &ucc_configs[value].cstate);
}
enum {
UCC_REMOVE_REQ,
UCC_UPDATE_REQ,
UCC_ADD_REQ,
};
static void list_add_priority(struct ucc_req *new, struct list_head *head)
{
struct list_head temp;
struct ucc_req *pivot;
/*
* If new request's value is bigger than first entry,
* add new request to first entry.
*/
pivot = list_first_entry(head, struct ucc_req, list);
if (pivot->value <= new->value) {
list_add(&new->list, head);
return;
}
/*
* If new request's value is smaller than last entry,
* add new request to last entry.
*/
pivot = list_last_entry(head, struct ucc_req, list);
if (pivot->value >= new->value) {
list_add_tail(&new->list, head);
return;
}
/*
* Find first entry that smaller than new request.
* And add new request before that entry.
*/
list_for_each_entry(pivot, head, list) {
if (pivot->value < new->value)
break;
}
list_cut_before(&temp, head, &pivot->list);
list_add(&new->list, head);
list_splice(&temp, head);
}
static void ucc_update(struct ucc_req *req, int value, int action)
{
int prev_value = ucc_get_value();
switch (action) {
case UCC_REMOVE_REQ:
list_del(&req->list);
req->active = 0;
break;
case UCC_UPDATE_REQ:
list_del(&req->list);
case UCC_ADD_REQ:
req->value = value;
list_add_priority(req, &ucc_req_list);
req->active = 1;
break;
}
if (prev_value != ucc_get_value())
update_cur_cstate();
}
void ucc_add_request(struct ucc_req *req, int value)
{
unsigned long flags;
if (!ucc_initialized)
return;
spin_lock_irqsave(&ucc_lock, flags);
if (req->active) {
spin_unlock_irqrestore(&ucc_lock, flags);
return;
}
ucc_update(req, value, UCC_ADD_REQ);
spin_unlock_irqrestore(&ucc_lock, flags);
}
EXPORT_SYMBOL_GPL(ucc_add_request);
void ucc_update_request(struct ucc_req *req, int value)
{
unsigned long flags;
if (!ucc_initialized)
return;
spin_lock_irqsave(&ucc_lock, flags);
if (!req->active) {
spin_unlock_irqrestore(&ucc_lock, flags);
return;
}
ucc_update(req, value, UCC_UPDATE_REQ);
spin_unlock_irqrestore(&ucc_lock, flags);
}
EXPORT_SYMBOL_GPL(ucc_update_request);
void ucc_remove_request(struct ucc_req *req)
{
unsigned long flags;
if (!ucc_initialized)
return;
spin_lock_irqsave(&ucc_lock, flags);
if (!req->active) {
spin_unlock_irqrestore(&ucc_lock, flags);
return;
}
ucc_update(req, 0, UCC_REMOVE_REQ);
spin_unlock_irqrestore(&ucc_lock, flags);
}
EXPORT_SYMBOL_GPL(ucc_remove_request);
static ssize_t ucc_requests_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct ucc_req *req;
int ret = 0;
list_for_each_entry(req, &ucc_req_list, list)
ret += snprintf(buf + ret, PAGE_SIZE - ret,
"request : %d (%s)\n", req->value, req->name);
return ret;
}
DEVICE_ATTR_RO(ucc_requests);
static struct ucc_req ucc_req =
{
.name = "ufcc",
};
static int ucc_requested;
static int ucc_requested_val;
static void ucc_control(int value)
{
ucc_update_request(&ucc_req, value);
ucc_requested_val = value;
}
static ssize_t cstate_control_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return snprintf(buf, 10, "%d\n", ucc_requested);
}
static ssize_t cstate_control_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
int input;
if (!sscanf(buf, "%8d", &input))
return -EINVAL;
if (input < 0)
return -EINVAL;
input = !!input;
if (input == ucc_requested)
return count;
ucc_requested = input;
if (ucc_requested)
ucc_add_request(&ucc_req, ucc_requested_val);
else
ucc_remove_request(&ucc_req);
return count;
}
DEVICE_ATTR_RW(cstate_control);
static int cstate_control_level;
static ssize_t cstate_control_level_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return snprintf(buf, 20, "%d (0=CPD, 1=C2)\n", cstate_control_level);
}
static ssize_t cstate_control_level_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t count)
{
int input;
if (!sscanf(buf, "%8d", &input))
return -EINVAL;
if (input < 0)
return -EINVAL;
cstate_control_level = input;
return count;
}
DEVICE_ATTR_RW(cstate_control_level);
static struct attribute *exynos_ucc_attrs[] = {
&dev_attr_ucc_requests.attr,
&dev_attr_cstate_control.attr,
&dev_attr_cstate_control_level.attr,
NULL,
};
static struct attribute_group exynos_ucc_group = {
.name = "ucc",
.attrs = exynos_ucc_attrs,
};
enum {
CPD_BLOCK,
C2_BLOCK
};
static int ucc_cpupm_notifier(struct notifier_block *nb,
unsigned long event, void *val)
{
int cpu = smp_processor_id();
if (!ucc_initialized)
return NOTIFY_OK;
if (event == C2_ENTER)
if (cstate_control_level == C2_BLOCK)
goto check;
if (event == CPD_ENTER)
if (cstate_control_level == CPD_BLOCK)
goto check;
return NOTIFY_OK;
check:
if (!cpumask_test_cpu(cpu, &cur_cstate)) {
/* not allow enter C2 */
return NOTIFY_BAD;
}
return NOTIFY_OK;
}
static struct notifier_block ucc_cpupm_nb = {
.notifier_call = ucc_cpupm_notifier,
};
static int exynos_ucc_init(struct platform_device *pdev)
{
struct device_node *dn, *child;
int ret, i = 0;
dn = of_find_node_by_name(pdev->dev.of_node, "ucc");
ucc_config_count = of_get_child_count(dn);
if (ucc_config_count == 0) {
pr_info("There is no ucc-config in DT\n");
return 0;
}
ucc_configs = kcalloc(ucc_config_count,
sizeof(struct ucc_config), GFP_KERNEL);
if (!ucc_configs) {
pr_err("Failed to alloc ucc_configs\n");
return -ENOMEM;
}
for_each_child_of_node(dn, child) {
const char *mask;
of_property_read_u32(child, "index", &ucc_configs[i].index);
if (of_property_read_string(child, "cstate", &mask))
cpumask_clear(&ucc_configs[i].cstate);
else
cpulist_parse(mask, &ucc_configs[i].cstate);
i++;
}
ret = sysfs_create_group(&pdev->dev.kobj, &exynos_ucc_group);
if (ret) {
pr_err("Failed to create cstate_control node\n");
kfree(ucc_configs);
return ret;
}
ret = exynos_cpupm_notifier_register(&ucc_cpupm_nb);
if (ret)
return ret;
cpumask_setall(&cur_cstate);
ucc_initialized = true;
return 0;
}
/*********************************************************************
* USER FREQUENCY CONTROL *
*********************************************************************/
static struct exynos_ufc {
unsigned int total_table_row; /* table row without validation */
unsigned int table_row; /* table row with validation*/
unsigned int table_col;
unsigned int lit_table_row;
unsigned int col_vfreq;
unsigned int col_big;
unsigned int col_mid;
unsigned int col_lit;
unsigned int col_emstune;
unsigned int col_over_limit;
int boot_domain_auto_table;
int max_vfreq;
int last_min_wo_boost_input;
/* The priority value of vfreq input */
int prio_vfreq[TYPE_END];
/* The determined max vfreq from max_strict and max*/
int curr_max_vfreq;
int strict_enabled;
ktime_t last_update_time;
u32 **ufc_lit_table;
struct list_head ufc_domain_list;
struct list_head ufc_table_list;
struct cpumask active_cpus;
struct mutex lock;
} ufc = {
.lock = __MUTEX_INITIALIZER(ufc.lock),
};
enum {
UFC_STATUS_NORMAL = 0,
UFC_STATUS_STRICT,
};
struct user_input {
char user_name[20];
int freq[TYPE_END];
u64 residency_time[TYPE_END];
};
static struct user_input ufc_req[USER_END];
static struct emstune_mode_request ufc_emstune_req;
/*********************************************************************
* HELPER FUNCTION *
*********************************************************************/
static void ufc_emstune_control(int level)
{
emstune_update_request(&ufc_emstune_req, level);
}
static char *get_user_type_string(enum ufc_user_type user)
{
switch (user) {
case USERSPACE:
return "USERSPACE";
case UFC_INPUT:
return "UFC_INPUT";
case FINGER:
return "FINGER";
case ARGOS:
return "ARGOS";
default:
return NULL;
}
return NULL;
}
static char *get_ctrl_type_string(enum ufc_ctrl_type type)
{
switch (type) {
case PM_QOS_MIN_LIMIT:
return "PM_QOS_MIN_LIMIT";
case PM_QOS_MIN_LIMIT_WO_BOOST:
return "PM_QOS_MIN_LIMIT_WO_BOOST";
case PM_QOS_MAX_LIMIT:
return "PM_QOS_MAX_LIMIT";
case PM_QOS_LITTLE_MAX_LIMIT:
return "PM_QOS_LITTLE_MAX_LIMIT";
case PM_QOS_OVER_LIMIT:
return "PM_QOS_OVER_LIMIT";
default:
return NULL;
}
return NULL;
}
static void
disable_domain_cpus(struct ufc_domain *ufc_dom)
{
if (!ufc_dom->allow_disable_cpus)
return;
cpumask_andnot(&ufc.active_cpus, &ufc.active_cpus, &ufc_dom->cpus);
ecs_request("UFC", &ufc.active_cpus);
}
static void
enable_domain_cpus(struct ufc_domain *ufc_dom)
{
if (!ufc_dom->allow_disable_cpus)
return;
cpumask_or(&ufc.active_cpus, &ufc.active_cpus, &ufc_dom->cpus);
ecs_request("UFC", &ufc.active_cpus);
}
unsigned int get_cpufreq_max_limit(void)
{
struct ufc_domain *ufc_dom;
struct cpufreq_policy *policy;
unsigned int freq_qos_max;
/* Big --> Mid --> Lit */
list_for_each_entry(ufc_dom, &ufc.ufc_domain_list, list) {
policy = cpufreq_cpu_get(cpumask_first(&ufc_dom->cpus));
if (!policy)
return -EINVAL;
/* get value of maximum PM QoS */
freq_qos_max = policy->max;
if (freq_qos_max > 0) {
freq_qos_max = min(freq_qos_max, ufc_dom->max_freq);
freq_qos_max = max(freq_qos_max, ufc_dom->min_freq);
return freq_qos_max;
}
}
/* no maximum PM QoS */
return 0;
}
EXPORT_SYMBOL(get_cpufreq_max_limit);
static unsigned int ufc_get_table_col(struct device_node *child)
{
struct device_node *dn;
int count_list = 0, ret = 0, col_idx;
dn = of_get_child_by_name(child, "table-info");
if (!dn)
return -EINVAL;
count_list = of_property_count_strings(dn, "table-order");
if (count_list <= 0)
return -EINVAL;
ufc.table_col = count_list;
for (col_idx = 0; col_idx < ufc.table_col; col_idx++) {
const char *col_name;
ret = of_property_read_string_index(dn, "table-order", col_idx, &col_name);
if (ret < 0)
return -EINVAL;
if (!strcmp("vfreq", col_name))
ufc.col_vfreq = col_idx;
else if (!strcmp("big", col_name))
ufc.col_big = col_idx;
else if (!strcmp("mid", col_name))
ufc.col_mid = col_idx;
else if (!strcmp("lit", col_name))
ufc.col_lit = col_idx;
else if (!strcmp("emstune", col_name))
ufc.col_emstune = col_idx;
else if (!strcmp("strict", col_name))
ufc.col_over_limit = col_idx;
}
return ret;
}
static unsigned int ufc_clamp_vfreq(unsigned int vfreq,
struct ufc_table_info *table_info)
{
unsigned int max_vfreq;
unsigned int min_vfreq;
max_vfreq = table_info->ufc_table[ufc.col_vfreq][0];
min_vfreq = table_info->ufc_table[ufc.col_vfreq][ufc.table_row + ufc.lit_table_row - 1];
if (max_vfreq < vfreq)
return max_vfreq;
if (min_vfreq > vfreq)
return min_vfreq;
return vfreq;
}
static int ufc_get_proper_min_table_index(unsigned int vfreq,
struct ufc_table_info *table_info)
{
int index;
vfreq = ufc_clamp_vfreq(vfreq, table_info);
for (index = 0; table_info->ufc_table[0][index] > vfreq; index++)
;
if (table_info->ufc_table[0][index] < vfreq)
index--;
return index;
}
static int ufc_get_proper_max_table_index(unsigned int vfreq,
struct ufc_table_info *table_info)
{
int index;
vfreq = ufc_clamp_vfreq(vfreq, table_info);
for (index = 0; table_info->ufc_table[0][index] > vfreq; index++)
;
return index;
}
static int ufc_get_proper_table_index(unsigned int vfreq,
struct ufc_table_info *table_info, int ctrl_type)
{
int target_idx = 0;
switch (ctrl_type) {
case PM_QOS_MIN_LIMIT:
case PM_QOS_MIN_LIMIT_WO_BOOST:
target_idx = ufc_get_proper_min_table_index(vfreq, table_info);
break;
case PM_QOS_MAX_LIMIT:
case PM_QOS_LITTLE_MAX_LIMIT:
case PM_QOS_OVER_LIMIT:
target_idx = ufc_get_proper_max_table_index(vfreq, table_info);
break;
}
return target_idx;
}
static unsigned int
ufc_adjust_freq(unsigned int freq, struct ufc_domain *dom, int ctrl_type)
{
if (freq > dom->max_freq)
return dom->max_freq;
if (freq < dom->min_freq) {
switch(ctrl_type) {
case PM_QOS_MIN_LIMIT:
case PM_QOS_MIN_LIMIT_WO_BOOST:
return dom->min_freq;
case PM_QOS_MAX_LIMIT:
case PM_QOS_LITTLE_MAX_LIMIT:
case PM_QOS_OVER_LIMIT:
if (freq == 0)
return 0;
return dom->min_freq;
}
}
return freq;
}
static unsigned int
ufc_get_release_freq(struct ufc_domain *dom, int ctrl_type)
{
switch(ctrl_type) {
case PM_QOS_MIN_LIMIT:
case PM_QOS_MIN_LIMIT_WO_BOOST:
return dom->min_freq;
case PM_QOS_MAX_LIMIT:
case PM_QOS_LITTLE_MAX_LIMIT:
case PM_QOS_OVER_LIMIT:
return dom->max_freq;
return 0;
}
return 0;
}
static struct freq_qos_request *
ufc_get_freq_qos_req(struct ufc_domain *dom, int ctrl_type)
{
switch(ctrl_type) {
case PM_QOS_MIN_LIMIT:
return &dom->user_min_qos_req;
case PM_QOS_MIN_LIMIT_WO_BOOST:
return &dom->user_min_qos_wo_boost_req;
case PM_QOS_MAX_LIMIT:
case PM_QOS_OVER_LIMIT:
return &dom->user_max_qos_req;
case PM_QOS_LITTLE_MAX_LIMIT:
return &dom->user_lit_max_qos_req;
return 0;
}
return 0;
}
static struct ufc_table_info *get_table_info(int ctrl_type)
{
struct ufc_table_info *table_info;
list_for_each_entry(table_info, &ufc.ufc_table_list, list)
if (table_info->ctrl_type == ctrl_type)
return table_info;
return NULL;
}
/*********************************************************************
* MULTI REQUEST MANAGEMENT *
*********************************************************************/
static void ufc_update_max_limit(void);
static int ufc_update_min_limit(void);
static int ufc_update_little_max_limit(void);
static void reset_limit_stat(void) {
int user, type;
for (user = 0; user < USER_END; user++) {
for (type = 0; type < TYPE_END; type++) {
ufc_req[user].residency_time[type] = 0;
}
}
}
static void update_limit_stat(void) {
int user, type, freq;
u64 duration = ktime_to_ms(ktime_get() - ufc.last_update_time);
ufc.last_update_time = ktime_get();
for (user = 0; user < USER_END; user++) {
for (type = 0; type < TYPE_END; type++) {
if (type == PM_QOS_OVER_LIMIT)
continue;
freq = ufc_req[user].freq[type];
if (freq > 0)
ufc_req[user].residency_time[type]
+= duration;
}
}
if (ufc.strict_enabled != UFC_STATUS_STRICT ||
ufc.prio_vfreq[PM_QOS_MAX_LIMIT] == RELEASE ||
ufc.prio_vfreq[PM_QOS_OVER_LIMIT] == RELEASE)
return;
for (user = 0; user < USER_END; user++) {
freq = ufc_req[user].freq[PM_QOS_MIN_LIMIT];
if (freq > 0)
ufc_req[user].residency_time[PM_QOS_OVER_LIMIT]
+= duration;
}
}
int ufc_update_request(enum ufc_user_type user, enum ufc_ctrl_type type, s32 new_freq)
{
s32 prev_freq;
int status_change = 0;
if (user < 0 || user >= USER_END || type < 0 || type >= TYPE_END)
return -EINVAL;
mutex_lock(&ufc.lock);
update_limit_stat();
prev_freq = ufc_req[user].freq[type];
ufc_req[user].freq[type] = new_freq;
/* Update Min Limit */
if (type == PM_QOS_MIN_LIMIT)
status_change = ufc_update_min_limit();
/* Update Max Limit */
if (type == PM_QOS_MAX_LIMIT || type == PM_QOS_OVER_LIMIT || status_change)
ufc_update_max_limit();
if (type == PM_QOS_LITTLE_MAX_LIMIT)
ufc_update_little_max_limit();
mutex_unlock(&ufc.lock);
return 0;
}
EXPORT_SYMBOL_GPL(ufc_update_request);
/*********************************************************************
* PM QOS CONTROL *
*********************************************************************/
static void ufc_release_freq(enum ufc_ctrl_type type)
{
struct ufc_domain *ufc_dom;
struct freq_qos_request *req;
unsigned int target_freq;
list_for_each_entry(ufc_dom, &ufc.ufc_domain_list, list) {
if (type == PM_QOS_LITTLE_MAX_LIMIT
&& ufc_dom->table_col_idx != ufc.col_lit)
continue;
target_freq = ufc_get_release_freq(ufc_dom, type);
req = ufc_get_freq_qos_req(ufc_dom, type);
if (!target_freq || !req)
return;
freq_qos_update_request(req, target_freq);
}
}
static int ufc_get_min_limit(void)
{
int user, freq, min_freq = RELEASE;
for (user = 0; user < USER_END; user++) {
freq = ufc_req[user].freq[PM_QOS_MIN_LIMIT];
if (freq == RELEASE)
continue;
/* Max Value among min_limit requests */
if (min_freq == RELEASE || freq > min_freq)
min_freq = freq;
}
return min_freq;
}
static int ufc_update_min_limit(void)
{
struct ufc_domain *ufc_dom;
struct ufc_table_info *table_info;
int target_freq, target_idx, level;
int prev_status;
table_info = get_table_info(PM_QOS_MIN_LIMIT);
if (!table_info) {
pr_err("failed to find target table\n");
return -EINVAL;
}
prev_status = ufc.strict_enabled;
target_freq = ufc_get_min_limit();
ufc.prio_vfreq[PM_QOS_MIN_LIMIT] = target_freq;
if (target_freq != RELEASE) {
target_idx = ufc_get_proper_table_index(target_freq,
table_info, PM_QOS_MIN_LIMIT);
/* Big ----> Lit */
list_for_each_entry(ufc_dom, &ufc.ufc_domain_list, list) {
unsigned int col_idx = ufc_dom->table_col_idx;
target_freq = table_info->ufc_table[col_idx][target_idx];
target_freq = ufc_adjust_freq(target_freq, ufc_dom, PM_QOS_MIN_LIMIT);
freq_qos_update_request(&ufc_dom->user_min_qos_req, target_freq);
}
level = table_info->ufc_table[ufc.col_emstune][target_idx];
if (table_info->ufc_table[ufc.col_over_limit][target_idx])
ufc.strict_enabled = UFC_STATUS_STRICT;
else
ufc.strict_enabled = UFC_STATUS_NORMAL;
} else {
ufc_release_freq(PM_QOS_MIN_LIMIT);
level = 0;
ufc.strict_enabled = UFC_STATUS_NORMAL;
}
ufc_emstune_control(level);
ucc_control(level);
return (prev_status != ufc.strict_enabled);
}
#define MAX(A, B) ((A) > (B) ? (A) : (B))
static int ufc_determine_max_limit(void)
{
int user, freq;
int max_limit = RELEASE, over_limit = RELEASE;
/* Update PM_QOS_MAX_LIMIT */
for (user = 0; user < USER_END; user++) {
freq = ufc_req[user].freq[PM_QOS_MAX_LIMIT];
if (freq == RELEASE)
continue;
/* Min Value among max_limit requests */
if (max_limit == RELEASE || freq < max_limit)
max_limit = freq;
}
ufc.prio_vfreq[PM_QOS_MAX_LIMIT] = max_limit;
/* Update PM_QOS_OVER_LIMIT */
for (user = 0; user < USER_END; user++) {
freq = ufc_req[user].freq[PM_QOS_OVER_LIMIT];
if (freq == RELEASE)
continue;
/* Min Value among over_limit requests */
if (over_limit == RELEASE || freq < over_limit)
over_limit = freq;
}
ufc.prio_vfreq[PM_QOS_OVER_LIMIT] = over_limit;
/* Determine the final max lmit */
if (ufc.strict_enabled != UFC_STATUS_STRICT)
return max_limit;
/* if max_limit is released, over_limit is also ignored */
if (max_limit == RELEASE)
return max_limit;
return MAX(max_limit, over_limit);
}
#define MIN(A, B) ((A) < (B) ? (A) : (B))
static int ufc_update_little_max_limit(void) {
struct ufc_domain *ufc_dom;
int user, target_freq = RELEASE, user_lit_max_freq;
for (user = 0; user < USER_END; user++) {
user_lit_max_freq = ufc_req[user].freq[PM_QOS_LITTLE_MAX_LIMIT];
if (user_lit_max_freq == RELEASE)
continue;
/* Min Value among little_max_limit requests */
if (target_freq == RELEASE || user_lit_max_freq < target_freq)
target_freq = user_lit_max_freq;
}
ufc.prio_vfreq[PM_QOS_LITTLE_MAX_LIMIT] = target_freq;
if (target_freq == RELEASE) {
ufc_release_freq(PM_QOS_LITTLE_MAX_LIMIT);
return 0;
}
list_for_each_entry(ufc_dom, &ufc.ufc_domain_list, list) {
unsigned int col_idx = ufc_dom->table_col_idx;
if (col_idx != ufc.col_lit)
continue;
freq_qos_update_request(&ufc_dom->user_lit_max_qos_req, target_freq);
}
return 0;
}
static void freq_qos_release(struct work_struct *work)
{
struct ufc_domain *ufc_dom;
if (!ufc.prio_vfreq[PM_QOS_LITTLE_MIN_LIMIT])
return;
ufc.prio_vfreq[PM_QOS_LITTLE_MIN_LIMIT] = 0;
list_for_each_entry(ufc_dom, &ufc.ufc_domain_list, list) {
unsigned int col_idx = ufc_dom->table_col_idx;
if (col_idx != ufc.col_lit)
continue;
freq_qos_update_request(&ufc_dom->hold_lit_max_qos_req, ufc_dom->hold_freq);
freq_qos_update_request(&ufc_dom->user_lit_min_qos_req, ufc_dom->min_freq);
break;
}
}
static void ufc_update_max_limit(void)
{
struct ufc_domain *ufc_dom;
struct ufc_table_info *table_info;
int target_freq, target_idx;
table_info = get_table_info(PM_QOS_MAX_LIMIT);
if (!table_info) {
pr_err("failed to find target table\n");
return;
}
target_freq = ufc_determine_max_limit();
ufc.curr_max_vfreq = target_freq;
/* If Max Limit is released, Max limit should be set as the orig max value */
if (target_freq == RELEASE) {
ufc_release_freq(PM_QOS_MAX_LIMIT);
return;
}
target_idx = ufc_get_proper_table_index(target_freq,
table_info, PM_QOS_MAX_LIMIT);
/* Big ----> Lit */
list_for_each_entry(ufc_dom, &ufc.ufc_domain_list, list) {
unsigned int col_idx = ufc_dom->table_col_idx;
target_freq = table_info->ufc_table[col_idx][target_idx];
target_freq = ufc_adjust_freq(target_freq, ufc_dom, PM_QOS_MAX_LIMIT);
if (target_freq == 0) {
freq_qos_update_request(&ufc_dom->user_max_qos_req, ufc_dom->min_freq);
disable_domain_cpus(ufc_dom);
continue;
}
enable_domain_cpus(ufc_dom);
freq_qos_update_request(&ufc_dom->user_max_qos_req, target_freq);
}
}
/*
* sysfs function
*/
static ssize_t cpufreq_table_show(struct kobject *kobj, char *buf)
{
struct ufc_table_info *table_info;
ssize_t count = 0;
int r_idx;
if (list_empty(&ufc.ufc_table_list))
return count;
table_info = list_first_entry(&ufc.ufc_table_list,
struct ufc_table_info, list);
for (r_idx = 0; r_idx < (ufc.table_row + ufc.lit_table_row); r_idx++)
count += snprintf(&buf[count], 10, "%d ",
table_info->ufc_table[0][r_idx]);
count += snprintf(&buf[count], 10, "\n");
return count;
}
static ssize_t cpufreq_max_limit_show(struct kobject *kobj, char *buf)
{
return snprintf(buf, 10, "%d\n", ufc.prio_vfreq[PM_QOS_MAX_LIMIT]);
}
static ssize_t cpufreq_max_limit_store(struct kobject *kobj,
const char *buf, size_t count)
{
return count;
}
static ssize_t cpufreq_min_limit_show(struct kobject *kobj, char *buf)
{
return snprintf(buf, 10, "%d\n", ufc.prio_vfreq[PM_QOS_MIN_LIMIT]);
}
static ssize_t limit_stat_store(struct kobject *kobj,
const char *buf, size_t count)
{
int input;
if (!sscanf(buf, "%8d", &input))
return -EINVAL;
mutex_lock(&ufc.lock);
reset_limit_stat();
mutex_unlock(&ufc.lock);
return count;
}
static ssize_t limit_stat_show(struct kobject *kobj, char *buf)
{
ssize_t count = 0;
int user, type;
mutex_lock(&ufc.lock);
update_limit_stat();
for (user = 0; user < USER_END; user++) {
for (type = 0; type < TYPE_END; type++) {
if (type == PM_QOS_MIN_LIMIT_WO_BOOST)
continue;
count += snprintf(&buf[count], 10, "%llu ",
ufc_req[user].residency_time[type]);
}
count += snprintf(&buf[count], 10, "\n");
}
mutex_unlock(&ufc.lock);
return count;
}
static ssize_t cpufreq_min_limit_store(struct kobject *kobj,
const char *buf, size_t count)
{
return count;
}
static ssize_t cpufreq_min_limit_wo_boost_show(struct kobject *kobj, char *buf)
{
return snprintf(buf, 10, "%d\n", ufc.last_min_wo_boost_input);
}
static ssize_t cpufreq_min_limit_wo_boost_store(struct kobject *kobj,
const char *buf, size_t count)
{
return count;
}
static ssize_t little_max_limit_show(struct kobject *kobj, char *buf)
{
return snprintf(buf, 10, "%d\n", ufc.prio_vfreq[PM_QOS_LITTLE_MAX_LIMIT]);
}
static ssize_t little_max_limit_store(struct kobject *kobj, const char *buf,
size_t count)
{
return count;
}
static ssize_t little_min_limit_show(struct kobject *kobj, char *buf)
{
return snprintf(buf, 10, "%d\n", ufc.prio_vfreq[PM_QOS_LITTLE_MIN_LIMIT]);
}
static ssize_t little_min_limit_store(struct kobject *kobj, const char *buf,
size_t count)
{
return count;
}
static ssize_t over_limit_show(struct kobject *kobj, char *buf)
{
return snprintf(buf, 10, "%d\n", ufc.prio_vfreq[PM_QOS_OVER_LIMIT]);
}
static ssize_t over_limit_store(struct kobject *kobj, const char *buf,
size_t count)
{
return count;
}
static int __debug_table_show(int ctrl_type, char *buf)
{
struct ufc_table_info *table_info;
int count = 0;
int c_idx, r_idx;
if (list_empty(&ufc.ufc_table_list))
return 0;
table_info = get_table_info(ctrl_type);
if (!table_info)
return -ENODEV;
count += snprintf(buf + count, PAGE_SIZE - count, "It's only for debug\n");
count += snprintf(buf + count, PAGE_SIZE - count, "Table Ctrl Type: %s(%d)\n",
get_ctrl_type_string(table_info->ctrl_type),
table_info->ctrl_type);
for (r_idx = 0; r_idx < (ufc.table_row + ufc.lit_table_row); r_idx++) {
for (c_idx = 0; c_idx < ufc.table_col; c_idx++) {
count += snprintf(buf + count, PAGE_SIZE - count, "%9d",
table_info->ufc_table[c_idx][r_idx]);
}
count += snprintf(buf + count, PAGE_SIZE - count,"\n");
}
return count;
}
static ssize_t debug_max_table_show(struct kobject *kobj, char *buf)
{
return __debug_table_show(PM_QOS_MAX_LIMIT, buf);
}
static ssize_t debug_min_table_show(struct kobject *kobj, char *buf)
{
return __debug_table_show(PM_QOS_MIN_LIMIT, buf);
}
static ssize_t info_show(struct kobject *kobj, char *buf)
{
ssize_t count = 0;
int user;
for (user = 0; user < USER_END; user++)
count += snprintf(&buf[count], 10, "%d\n",
ufc_req[user].freq[PM_QOS_MIN_LIMIT]);
return count;
}
static ssize_t debug_table_col_show(struct kobject *kobj, char *buf)
{
int ret = 0;
ret += sprintf(buf + ret, "tatal_col:%d vfreq:%d lit:%d mid:%d big:%d emstune:%d over_limit:%d ",
ufc.table_col, ufc.col_vfreq, ufc.col_big, ufc.col_mid, ufc.col_lit,
ufc.col_emstune, ufc.col_over_limit);
return ret;
}
struct ufc_attr {
struct attribute attr;
ssize_t (*show)(struct kobject *, char *);
ssize_t (*store)(struct kobject *, const char *, size_t);
};
#define UFC_ATTR_RO(name) \
static struct ufc_attr attr_##name \
= __ATTR(name, 0444, name##_show, NULL)
#define UFC_ATTR_RW(name) \
static struct ufc_attr attr_##name \
= __ATTR(name, 0644, name##_show, name##_store)
UFC_ATTR_RO(cpufreq_table);
UFC_ATTR_RW(limit_stat);
UFC_ATTR_RW(cpufreq_min_limit);
UFC_ATTR_RW(cpufreq_min_limit_wo_boost);
UFC_ATTR_RW(cpufreq_max_limit);
UFC_ATTR_RW(over_limit);
UFC_ATTR_RW(little_max_limit);
UFC_ATTR_RW(little_min_limit);
UFC_ATTR_RO(debug_max_table);
UFC_ATTR_RO(debug_min_table);
UFC_ATTR_RO(info);
UFC_ATTR_RO(debug_table_col);
static struct attribute *exynos_ufc_attrs[] = {
&attr_cpufreq_table.attr,
&attr_cpufreq_min_limit.attr,
&attr_cpufreq_min_limit_wo_boost.attr,
&attr_cpufreq_max_limit.attr,
&attr_over_limit.attr,
&attr_little_max_limit.attr,
&attr_little_min_limit.attr,
&attr_limit_stat.attr,
&attr_debug_max_table.attr,
&attr_debug_min_table.attr,
&attr_info.attr,
&attr_debug_table_col.attr,
NULL,
};
#define attr_to_ufcattr(a) container_of(a, struct ufc_attr, attr)
static ssize_t ufc_sysfs_show(struct kobject *kobj, struct attribute *at, char *buf)
{
struct ufc_attr *ufcattr = attr_to_ufcattr(at);
int ret = 0;
if (ufcattr->show)
ret = ufcattr->show(kobj, buf);
return ret;
}
static ssize_t ufc_sysfs_store(struct kobject *kobj, struct attribute *at,
const char *buf, size_t count)
{
struct ufc_attr *ufcattr = attr_to_ufcattr(at);
int ret = 0;
if (ufcattr->show)
ret = ufcattr->store(kobj, buf, count);
return ret;
}
static const struct sysfs_ops ufc_sysfs_ops = {
.show = ufc_sysfs_show,
.store = ufc_sysfs_store,
};
static struct kobj_type ktype_ufc = {
.sysfs_ops = &ufc_sysfs_ops,
.default_attrs = exynos_ufc_attrs,
};
struct kobject ufc_kobj;
/*********************************************************************
* INIT FUNCTION *
*********************************************************************/
static void ufc_free_all(void)
{
struct ufc_table_info *table_info;
struct ufc_domain *ufc_dom;
int col_idx;
pr_err("Failed: can't initialize ufc table and domain");
while(!list_empty(&ufc.ufc_table_list)) {
table_info = list_first_entry(&ufc.ufc_table_list,
struct ufc_table_info, list);
list_del(&table_info->list);
for (col_idx = 0; table_info->ufc_table[col_idx]; col_idx++)
kfree(table_info->ufc_table[col_idx]);
kfree(table_info->ufc_table);
kfree(table_info);
}
while(!list_empty(&ufc.ufc_domain_list)) {
ufc_dom = list_first_entry(&ufc.ufc_domain_list,
struct ufc_domain, list);
list_del(&ufc_dom->list);
kfree(ufc_dom);
}
}
static void ufc_init_user(void)
{
int user, type;
for (user = 0; user < USER_END; user++) {
strcpy(ufc_req[user].user_name, get_user_type_string(user));
for (type = 0 ; type < TYPE_END; type++) {
ufc_req[user].freq[type] = RELEASE;
ufc_req[user].residency_time[type] = 0;
}
}
}
static int ufc_init_freq_qos(void)
{
struct cpufreq_policy *policy;
struct ufc_domain *ufc_dom;
list_for_each_entry(ufc_dom, &ufc.ufc_domain_list, list) {
policy = cpufreq_cpu_get(cpumask_first(&ufc_dom->cpus));
if (!policy)
return -EINVAL;
freq_qos_tracer_add_request(&policy->constraints, &ufc_dom->user_min_qos_req,
FREQ_QOS_MIN, policy->cpuinfo.min_freq);
freq_qos_tracer_add_request(&policy->constraints, &ufc_dom->user_max_qos_req,
FREQ_QOS_MAX, policy->cpuinfo.max_freq);
freq_qos_tracer_add_request(&policy->constraints, &ufc_dom->user_min_qos_wo_boost_req,
FREQ_QOS_MIN, policy->cpuinfo.min_freq);
if (ufc_dom->table_col_idx == ufc.col_lit) {
freq_qos_tracer_add_request(&policy->constraints, &ufc_dom->user_lit_min_qos_req,
FREQ_QOS_MIN, policy->cpuinfo.min_freq);
freq_qos_tracer_add_request(&policy->constraints, &ufc_dom->user_lit_max_qos_req,
FREQ_QOS_MAX, policy->cpuinfo.max_freq);
freq_qos_tracer_add_request(&policy->constraints, &ufc_dom->hold_lit_max_qos_req,
FREQ_QOS_MAX, ufc_dom->hold_freq);
}
}
/* Success */
return 0;
}
static int ufc_init_sysfs(struct kobject *kobj)
{
int ret = 0;
/* /sys/devices/platform/exynos-ufcc/ufc/ */
if (kobject_init_and_add(&ufc_kobj, &ktype_ufc,
kobj, "ufc"))
pr_err("Failed to init exynos ufc\n");
/* /sys/devices/system/cpu/cpufreq_limit */
if (sysfs_create_link(&cpu_subsys.dev_root->kobj, &ufc_kobj, "cpufreq_limit"))
pr_err("Failed to link UFC sysfs to cpuctrl \n");
return ret;
}
static int ufc_parse_init_table(struct ufc_table_info *ufc_info,
struct cpufreq_policy *policy, u32 *freq_table, u32 *check_valid_row)
{
int col_idx, row_idx, invalid_row = 0, row_backup;
struct cpufreq_frequency_table *pos;
for (row_idx = 0; row_idx < ufc.total_table_row; row_idx++) {
for (col_idx = 0; col_idx < ufc.table_col; col_idx++) {
if (!check_valid_row[row_idx]) {
invalid_row++;
break;
}
ufc_info->ufc_table[col_idx][row_idx - invalid_row]
= freq_table[(ufc.table_col * row_idx) + col_idx];
}
}
if (!policy)
return 0;
cpufreq_for_each_entry(pos, policy->freq_table) {
if (pos->frequency > policy->max)
continue;
}
row_backup = (row_idx - invalid_row) + ufc.lit_table_row - 1;
for (col_idx = 0; col_idx < ufc.table_col; col_idx++) {
row_idx = row_backup;
cpufreq_for_each_entry(pos, policy->freq_table) {
if (pos->frequency > policy->max)
continue;
#define SCALE_SIZE 8
if (col_idx == ufc.col_vfreq)
ufc_info->ufc_table[col_idx][row_idx--]
= pos->frequency / SCALE_SIZE;
else if (col_idx == ufc.col_lit)
ufc_info->ufc_table[col_idx][row_idx--]
= pos->frequency;
else
ufc_info->ufc_table[col_idx][row_idx--] = 0;
}
ufc.max_vfreq = ufc_info->ufc_table[ufc.col_vfreq][0];
}
/* Success */
return 0;
}
static struct cpufreq_policy *bootcpu_policy;
static int init_ufc_table(struct device_node *dn)
{
struct ufc_domain *ufc_dom;
struct ufc_table_info *table_info;
u32 *check_valid_row, *table_wo_validation;
int col_idx = 0, row_idx, size, loc, ret = 0;
unsigned int max_freq, min_freq, freq, valid_row = 0;
table_info = kzalloc(sizeof(struct ufc_table_info), GFP_KERNEL);
if (!table_info)
return -ENOMEM;
list_add_tail(&table_info->list, &ufc.ufc_table_list);
if (of_property_read_u32(dn, "ctrl-type", &table_info->ctrl_type))
return -EINVAL;
size = of_property_count_u32_elems(dn, "table");
if (size < 0)
return size;
table_wo_validation = kcalloc(size, sizeof(u32), GFP_KERNEL);
if (!table_wo_validation)
return -ENOMEM;
if (of_property_read_u32_array(dn, "table", table_wo_validation, size)) {
ret = -EINVAL;
goto out_table;
}
check_valid_row = kzalloc(sizeof(u32) * (size / ufc.table_col), GFP_KERNEL);
if (!check_valid_row) {
ret = -ENOMEM;
goto out_table;
}
list_for_each_entry(ufc_dom, &ufc.ufc_domain_list, list) {
max_freq = ufc_dom->max_freq;
min_freq = ufc_dom->min_freq;
col_idx++;
for (row_idx = 0; row_idx < ufc.total_table_row; row_idx++) {
loc = (ufc.table_col * row_idx) + col_idx;
freq = table_wo_validation[loc];
if (freq > max_freq)
table_wo_validation[loc] = max_freq;
else if (freq < min_freq)
table_wo_validation[loc] = min_freq;
else if (col_idx != ufc.col_lit)
check_valid_row[row_idx]++;
}
}
for (row_idx = 0; row_idx < ufc.total_table_row; row_idx++) {
if (check_valid_row[row_idx] > 0)
valid_row++;
}
ufc.table_row = valid_row;
table_info->ufc_table = kzalloc(sizeof(u32 *) * ufc.table_col, GFP_KERNEL);
if (!table_info->ufc_table) {
ret = -ENOMEM;
goto out_all;
}
for (col_idx = 0; col_idx < ufc.table_col; col_idx++) {
table_info->ufc_table[col_idx] = kzalloc(sizeof(u32) *
(ufc.table_row + ufc.lit_table_row + 1), GFP_KERNEL);
if (!table_info->ufc_table[col_idx]) {
ret = -ENOMEM;
goto out_all;
}
}
ufc_parse_init_table(table_info, bootcpu_policy, table_wo_validation, check_valid_row);
out_all:
kfree(check_valid_row);
out_table:
kfree(table_wo_validation);
return ret;
}
static int init_ufc_domain(struct device_node *dn)
{
struct ufc_domain *ufc_dom;
struct cpufreq_policy *policy;
const char *buf;
ufc_dom = kzalloc(sizeof(struct ufc_domain), GFP_KERNEL);
if (!ufc_dom)
return -ENOMEM;
list_add(&ufc_dom->list, &ufc.ufc_domain_list);
if (of_property_read_string(dn, "shared-cpus", &buf))
return -EINVAL;
cpulist_parse(buf, &ufc_dom->cpus);
cpumask_and(&ufc_dom->cpus, &ufc_dom->cpus, cpu_online_mask);
if (cpumask_empty(&ufc_dom->cpus)) {
list_del(&ufc_dom->list);
kfree(ufc_dom);
return 0;
}
if (of_property_read_u32(dn, "table-col-idx", &ufc_dom->table_col_idx))
return -EINVAL;
policy = cpufreq_cpu_get(cpumask_first(&ufc_dom->cpus));
if (!policy)
return -EINVAL;
of_property_read_u32(dn, "hold-freq", &ufc_dom->hold_freq);
if (!ufc_dom->hold_freq)
ufc_dom->hold_freq = policy->cpuinfo.max_freq;
ufc_dom->min_freq = policy->cpuinfo.min_freq;
ufc_dom->max_freq = policy->cpuinfo.max_freq;
of_property_read_u32(dn, "little-min-timeout", &ufc_dom->little_min_timeout);
if (ufc_dom->little_min_timeout)
INIT_DELAYED_WORK(&ufc_dom->work, freq_qos_release);
#define BOOTCPU 0
if (cpumask_test_cpu(BOOTCPU, &ufc_dom->cpus)
&& ufc.boot_domain_auto_table) {
struct cpufreq_frequency_table *pos;
int lit_row = 0;
bootcpu_policy = policy;
cpufreq_for_each_entry(pos, policy->freq_table) {
if (pos->frequency > policy->max)
continue;
lit_row++;
}
ufc.lit_table_row = lit_row;
}
cpufreq_cpu_put(policy);
if (of_property_read_bool(dn, "allow-disable-cpus"))
ufc_dom->allow_disable_cpus = 1;
/* Success */
return 0;
}
static int init_ufc(struct device_node *dn)
{
struct device_node *table_node;
int size, type, ret = 0;
INIT_LIST_HEAD(&ufc.ufc_domain_list);
INIT_LIST_HEAD(&ufc.ufc_table_list);
ret = ufc_get_table_col(dn);
if (ret != 0)
return -EINVAL;
table_node = of_find_node_by_type(dn, "ufc-table");
size = of_property_count_u32_elems(table_node, "table");
ufc.total_table_row = size / ufc.table_col;
if (of_property_read_bool(dn, "boot-domain-auto-table"))
ufc.boot_domain_auto_table = 1;
ufc.last_min_wo_boost_input = RELEASE;
for (type = 0; type < TYPE_END; type++)
ufc.prio_vfreq[type] = RELEASE;
ufc.curr_max_vfreq = RELEASE;
ufc.strict_enabled = UFC_STATUS_NORMAL;
emstune_add_request(&ufc_emstune_req);
return ret;
}
static int exynos_ufc_init(struct platform_device *pdev)
{
int cpu;
struct device_node *dn;
dn = of_find_node_by_name(pdev->dev.of_node, "ufc");
/* check whether cpufreq is registered or not */
for_each_online_cpu(cpu) {
if (!cpufreq_cpu_get(0))
return 0;
}
if (init_ufc(dn)) {
pr_err("exynos-ufc: Failed to init init_ufc\n");
return 0;
}
dn = NULL;
while((dn = of_find_node_by_type(dn, "ufc-domain"))) {
if (init_ufc_domain(dn)) {
pr_err("exynos-ufc: Failed to init init_ufc_domain\n");
ufc_free_all();
return 0;
}
}
dn = NULL;
while((dn = of_find_node_by_type(dn, "ufc-table"))) {
if (init_ufc_table(dn)) {
pr_err("exynos-ufc: Failed to init init_ufc_table\n");
ufc_free_all();
return 0;
}
}
if (ufc_init_sysfs(&pdev->dev.kobj)){
pr_err("exynos-ufc: Failed to init sysfs\n");
ufc_free_all();
return 0;
}
if (ufc_init_freq_qos()) {
pr_err("exynos-ufc: Failed to init freq_qos\n");
ufc_free_all();
return 0;
}
cpumask_setall(&ufc.active_cpus);
if (ecs_request_register("UFC", &ufc.active_cpus)) {
pr_err("exynos-ufc: Failed to register ecs\n");
ufc_free_all();
return 0;
}
ufc_init_user();
pr_info("exynos-ufc: Complete UFC driver initialization\n");
return 0;
}
/*********************************************************************
* DRIVER INITIALIZATION *
*********************************************************************/
static int exynos_ufcc_probe(struct platform_device *pdev)
{
exynos_ucc_init(pdev);
exynos_ufc_init(pdev);
return 0;
}
static const struct of_device_id of_exynos_ufcc_match[] = {
{ .compatible = "samsung,exynos-ufcc", },
{ },
};
MODULE_DEVICE_TABLE(of, of_exynos_ufcc_match);
static struct platform_driver exynos_ufcc_driver = {
.driver = {
.name = "exynos-ufcc",
.owner = THIS_MODULE,
.of_match_table = of_exynos_ufcc_match,
},
.probe = exynos_ufcc_probe,
};
static int __init exynos_ufcc_init(void)
{
return platform_driver_register(&exynos_ufcc_driver);
}
late_initcall(exynos_ufcc_init);
static void __exit exynos_ufcc_exit(void)
{
platform_driver_unregister(&exynos_ufcc_driver);
}
module_exit(exynos_ufcc_exit);
MODULE_SOFTDEP("pre: exynos-acme");
MODULE_DESCRIPTION("Exynos UFCC driver");
MODULE_LICENSE("GPL");