1709 lines
40 KiB
C
Executable file
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");
|