| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (c) 2020 MediaTek Inc. |
| */ |
| #include <linux/cpufreq.h> |
| #include <linux/device.h> |
| #include <linux/err.h> |
| #include <linux/io.h> |
| #include <linux/jiffies.h> |
| #include <linux/kernel.h> |
| #include <linux/kobject.h> |
| #include <linux/ktime.h> |
| #include <linux/list.h> |
| #include <linux/module.h> |
| #include <linux/mutex.h> |
| #include <linux/of.h> |
| #include <linux/of_platform.h> |
| #include <linux/platform_device.h> |
| #include <linux/pm_qos.h> |
| #include <linux/proc_fs.h> |
| #include <linux/slab.h> |
| #include <linux/sysfs.h> |
| #include <linux/thermal.h> |
| #include <linux/time64.h> |
| #include <linux/timekeeping.h> |
| #include <linux/types.h> |
| #include <linux/workqueue.h> |
| #include "thermal_interface.h" |
| #include "thermal_jatm.h" |
| #include "thermal_trace.h" |
| |
| |
| static LIST_HEAD(jatm_policy_list); |
| |
| struct jatm_info { |
| bool turn_on; |
| bool activated; |
| int max_budget; |
| int budget; |
| int interval; |
| int stop_deadline; |
| int mode; |
| bool fix_opp; |
| bool delay_start; |
| struct timespec64 last_jatm_enable; |
| struct timespec64 last_frame_start; |
| }; |
| |
| static struct jatm_info j_info; |
| |
| DEFINE_MUTEX(jatm_mutex); |
| |
| static void jatm_stop_work_fn(struct work_struct *work); |
| static void jatm_start_work_fn(struct work_struct *work); |
| static DECLARE_DELAYED_WORK(jatm_stop_work, jatm_stop_work_fn); |
| static DECLARE_DELAYED_WORK(jatm_start_work, jatm_start_work_fn); |
| |
| LIST_HEAD(jatm_record_list); |
| |
| static void jatm_set_deadline(void) |
| { |
| schedule_delayed_work(&jatm_stop_work, |
| msecs_to_jiffies(j_info.stop_deadline)); |
| } |
| |
| static unsigned long calculate_timeval_diff(struct timespec64 *start, struct timespec64 *end) |
| { |
| unsigned long ms; |
| |
| ms = (end->tv_sec - start->tv_sec) * 1000; |
| ms += ((end->tv_nsec - start->tv_nsec) / 1000000); |
| return ms; |
| } |
| |
| static bool jatm_is_enabled(void) |
| { |
| return j_info.activated; |
| } |
| |
| static void cpu_force_max_freq(int enable) |
| { |
| struct jatm_policy *j_policy; |
| s32 freq_limit; |
| |
| if (j_info.fix_opp == false) |
| return; |
| |
| if (enable == 1) |
| freq_limit = FREQ_QOS_MAX_DEFAULT_VALUE; |
| else |
| freq_limit = FREQ_QOS_MIN_DEFAULT_VALUE; |
| |
| list_for_each_entry(j_policy, &jatm_policy_list, jatm_list) |
| freq_qos_update_request(&j_policy->qos_req, freq_limit); |
| } |
| |
| static void schedule_jatm_work(void) |
| { |
| schedule_delayed_work(&jatm_stop_work, msecs_to_jiffies(j_info.budget)); |
| } |
| |
| |
| static void enable_jatm(void) |
| { |
| j_info.activated = true; |
| /* 1. */ |
| set_ttj(JATM_ON); |
| /* 2. */ |
| cpu_force_max_freq(1); |
| /* 3. */ |
| |
| pr_info("Enabled and remaining budget = %d\n", j_info.budget); |
| trace_jatm_enable(j_info.budget); |
| |
| if (j_info.mode == BUDGET) |
| schedule_jatm_work(); |
| else if (j_info.mode == STOP_DEADLINE) |
| jatm_set_deadline(); |
| ktime_get_real_ts64(&(j_info.last_jatm_enable)); |
| } |
| |
| static void remove_all_records(void) |
| { |
| struct jatm_record *record, *tmp; |
| |
| list_for_each_entry_safe(record, tmp, &jatm_record_list, list) { |
| list_del(&record->list); |
| kfree(record); |
| } |
| } |
| |
| static void recycle_jatm_budget(void) |
| { |
| struct jatm_record *record, *tmp; |
| struct timespec64 now_tv; |
| unsigned long ms; |
| |
| if (likely(list_empty(&jatm_record_list))) |
| return; |
| |
| ktime_get_real_ts64(&now_tv); |
| record = list_last_entry(&jatm_record_list, struct jatm_record, list); |
| ms = calculate_timeval_diff(&record->end_tv, &now_tv); |
| |
| /* elapsed more than jatm_interval since end time of last trigger, |
| * jatm budget is fully recovered, recycle all entries |
| */ |
| pr_debug("%s jatm_budget=%d elapsed=%d jatm_interval=%d\n", __func__, |
| j_info.budget, ms, j_info.interval); |
| if (ms >= j_info.interval) { |
| pr_debug("Budget is fully recoveried\n"); |
| remove_all_records(); |
| j_info.budget = j_info.max_budget; |
| } else { |
| /* only recovery record ends more than jatm_interval */ |
| list_for_each_entry_safe(record, tmp, &jatm_record_list, list) { |
| ms = calculate_timeval_diff(&record->end_tv, &now_tv); |
| if (ms >= j_info.interval) { |
| pr_debug("recycle jatm_budget=%d\n", record->usage); |
| j_info.budget += record->usage; |
| list_del(&record->list); |
| kfree(record); |
| } else { |
| /* sort after current entry must newer than |
| * current entry, no need to traverse them |
| */ |
| break; |
| } |
| } |
| if (j_info.budget > j_info.max_budget) { |
| pr_notice("JATM budget(%d) > max_budget(%d)\n", |
| j_info.budget, j_info.max_budget); |
| j_info.budget = j_info.max_budget; |
| } |
| } |
| } |
| |
| void jatm_start_work_fn(struct work_struct *work) |
| { |
| struct timespec64 now_tv; |
| int target_tj; |
| enum jatm_not_start_reason reason; |
| |
| reason = ENABLE; |
| |
| ktime_get_real_ts64(&now_tv); |
| |
| mutex_lock(&jatm_mutex); |
| if (unlikely(jatm_is_enabled())) { |
| /*In case reentant happens, don't reset jatm timer, since CPU*/ |
| /*is already being heat up because of opp0*/ |
| pr_notice("JATM cannot enable: already enabled\n"); |
| reason = ALREADY_ENABLED; |
| goto done_unlock; |
| } |
| if (j_info.mode == STOP_DEADLINE) { |
| target_tj = get_catm_ttj(); |
| if (unlikely(target_tj <= get_catm_min_ttj())) { |
| pr_notice("JATM cannot enable: min TTJ\n"); |
| reason = MIN_TTJ; |
| goto done_unlock; |
| } |
| |
| if (get_jatm_suspend()) { |
| pr_notice("JATM cannot enable: being suspended\n"); |
| reason = SUSPENDED; |
| goto done_unlock; |
| } |
| } |
| /*recycle_jatm_budget just before enabling instead of*/ |
| /*try_enable_jatm() to have maximum budget*/ |
| recycle_jatm_budget(); |
| |
| |
| if ((j_info.mode == STOP_DEADLINE) || (j_info.budget > 0)) |
| enable_jatm(); |
| else { |
| pr_notice("JATM cannot enable: no budget\n"); |
| reason = NO_BUDGET; |
| } |
| |
| |
| done_unlock: |
| mutex_unlock(&jatm_mutex); |
| trace_not_start_reason(reason); |
| } |
| |
| |
| static void try_enable_jatm(void) |
| { |
| int delay, elapsed; |
| struct timespec64 now_tv; |
| |
| mutex_lock(&jatm_mutex); |
| ktime_get_real_ts64(&now_tv); |
| /* delay based on jatm usage in the past one period */ |
| if (j_info.delay_start) { |
| /* recycle here to get accurate jatm usage */ |
| recycle_jatm_budget(); |
| if (j_info.budget < 0) |
| delay = j_info.interval; |
| else |
| delay = j_info.max_budget - j_info.budget; |
| } else { /* delay based on jatm usage in the past one period */ |
| delay = 0; |
| } |
| elapsed = calculate_timeval_diff(&j_info.last_frame_start, &now_tv); |
| pr_notice("Try to enable JATM after %d ms and delay start %d ms\n", elapsed, delay); |
| trace_try_enable_jatm(elapsed, delay); |
| |
| if (delay > 0) { |
| if (delayed_work_pending(&jatm_start_work)) |
| mod_delayed_work(system_wq, &jatm_start_work, |
| msecs_to_jiffies(delay)); |
| else |
| schedule_delayed_work(&jatm_start_work, |
| msecs_to_jiffies(delay)); |
| |
| mutex_unlock(&jatm_mutex); |
| } else { |
| if (delayed_work_pending(&jatm_start_work)) |
| cancel_delayed_work(&jatm_start_work); |
| /* jatm_start_work_fn will hold the lock, release here */ |
| mutex_unlock(&jatm_mutex); |
| jatm_start_work_fn(NULL); |
| } |
| |
| } |
| |
| |
| static void record_jatm_usage(struct timespec64 now, int elapsed_ms) |
| { |
| struct jatm_record *record = kmalloc(sizeof(struct jatm_record), GFP_KERNEL); |
| |
| if (!record) |
| return; |
| |
| record->usage = elapsed_ms; |
| record->end_tv = now; |
| list_add_tail(&(record->list), &jatm_record_list); |
| } |
| |
| static void disable_jatm(enum jatm_stop_reason reason) |
| { |
| struct timespec64 now_tv; |
| unsigned long jatm_usage, real_usage; |
| unsigned long frame_length; |
| |
| mutex_lock(&jatm_mutex); |
| ktime_get_real_ts64(&now_tv); |
| frame_length = calculate_timeval_diff(&j_info.last_frame_start, &now_tv); |
| /* the hook point in the fpsgo now is almost the start of the next frame */ |
| j_info.last_frame_start = now_tv; |
| |
| cancel_delayed_work(&jatm_start_work); |
| /* cancel max freq if we are during JATM, false means JATM not enabled */ |
| if (unlikely(!jatm_is_enabled())) { |
| mutex_unlock(&jatm_mutex); |
| return; |
| } |
| |
| j_info.activated = false; |
| write_jatm_suspend(1); |
| |
| real_usage = calculate_timeval_diff(&j_info.last_jatm_enable, &now_tv); |
| |
| if (real_usage < 5) { |
| pr_debug("JATM usage %d < min usage 5\n", real_usage); |
| jatm_usage = 5; |
| } else |
| jatm_usage = real_usage; |
| |
| |
| if (j_info.budget < jatm_usage) |
| j_info.budget = 0; |
| else |
| j_info.budget -= jatm_usage; |
| record_jatm_usage(now_tv, jatm_usage); |
| cancel_delayed_work(&jatm_stop_work); |
| set_ttj(JATM_OFF); |
| |
| cpu_force_max_freq(0); |
| mutex_unlock(&jatm_mutex); |
| pr_info("stop reason=%s, frame_length=%d, real_usage=%d, remaining=%d\n", |
| jatm_stop_reason_string[reason], frame_length, real_usage, j_info.budget); |
| trace_jatm_disable(reason, frame_length, real_usage, j_info.budget); |
| } |
| |
| static void jatm_stop_work_fn(struct work_struct *work) |
| { |
| disable_jatm(BUDGET_RUNNING_OUT); |
| } |
| |
| void jatm_notify_fp_cb(int enable) |
| { |
| |
| if (j_info.turn_on != true) |
| return; |
| |
| if (enable == 1) |
| try_enable_jatm(); |
| else |
| disable_jatm(FRAME_COMPLETE); |
| } |
| |
| static ssize_t jatm_info_show(struct kobject *kobj, |
| struct kobj_attribute *attr, char *buf) |
| { |
| int len = 0; |
| |
| len += snprintf(buf + len, PAGE_SIZE - len, "%d, %d, %d, %d, %d, %d, %d %d\n", |
| j_info.max_budget, |
| j_info.budget, |
| j_info.interval, |
| j_info.fix_opp, |
| jatm_is_enabled(), |
| j_info.stop_deadline, |
| j_info.delay_start, |
| j_info.mode |
| ); |
| |
| return len; |
| } |
| |
| static ssize_t jatm_info_store(struct kobject *kobj, |
| struct kobj_attribute *attr, const char *buf, size_t count) |
| { |
| char cmd[10]; |
| int max_budget, budget, interval, fix_opp; |
| int jatm_stop_deadline, jatm_delay_start; |
| int mode; |
| |
| if (sscanf(buf, "%5s %u %u %u %u %u %u %u", cmd, |
| &max_budget, &budget, &interval, &fix_opp, |
| &jatm_stop_deadline, &jatm_delay_start, &mode) == 8) { |
| |
| if (strncmp(cmd, "JATM", 4) != 0) { |
| pr_info("[jatm_info] invalid input\n"); |
| return -EINVAL; |
| } |
| if ((budget < 0) || (budget > 50000)) { |
| pr_info("[jatm_info] budget should be 0 ~ 50000\n"); |
| return -EINVAL; |
| } |
| if ((max_budget < 0) || (max_budget > 50000)) { |
| pr_info("[jatm_info] max_budget should be 0 ~ 50000\n"); |
| return -EINVAL; |
| } |
| if (max_budget < budget) { |
| pr_info("[jatm_info] max_budget should be >= budget\n"); |
| return -EINVAL; |
| } |
| if ((interval < 0) || (interval > 10000)) { |
| pr_info("[jatm_info] interval should be 0 ~ 10000\n"); |
| return -EINVAL; |
| } |
| |
| if ((jatm_stop_deadline < 0) || (jatm_stop_deadline > 500)) { |
| pr_info("[jatm_info] jatm_stop_deadline should be 0 ~ 500\n"); |
| return -EINVAL; |
| } |
| j_info.max_budget = max_budget; |
| j_info.budget = budget; |
| j_info.interval = interval; |
| if (fix_opp == 1) |
| j_info.fix_opp = true; |
| else |
| j_info.fix_opp = false; |
| j_info.stop_deadline = jatm_stop_deadline; |
| if (jatm_delay_start == 1) |
| j_info.delay_start = true; |
| else |
| j_info.delay_start = false; |
| |
| j_info.mode = mode; |
| return count; |
| |
| } |
| |
| pr_info("[jatm_info] invalid input\n"); |
| |
| return -EINVAL; |
| } |
| |
| static ssize_t turn_on_show(struct kobject *kobj, |
| struct kobj_attribute *attr, char *buf) |
| { |
| int len = 0; |
| |
| len += snprintf(buf + len, PAGE_SIZE - len, "%d\n", |
| j_info.turn_on); |
| |
| return len; |
| } |
| |
| static ssize_t turn_on_store(struct kobject *kobj, |
| struct kobj_attribute *attr, const char *buf, size_t count) |
| { |
| char cmd[10]; |
| int activate; |
| int turn_on; |
| |
| if (sscanf(buf, "%8s %d", cmd, &activate) == 2) { |
| if (strncmp(cmd, "JATM_UT", 7) == 0) { |
| if (activate == 1) |
| try_enable_jatm(); |
| else if (activate == 0) |
| disable_jatm(FRAME_COMPLETE); |
| return count; |
| } |
| } |
| if (kstrtoint(buf, 10, &turn_on) == 0) { |
| if (turn_on == 1) |
| j_info.turn_on = true; |
| else if (turn_on == 0) |
| j_info.turn_on = false; |
| |
| pr_info("[jatm_enable] turn_on %d\n", j_info.turn_on); |
| return count; |
| } |
| |
| pr_info("[jatm_enable] invalid input\n"); |
| |
| return -EINVAL; |
| } |
| |
| static ssize_t max_budget_store(struct kobject *kobj, |
| struct kobj_attribute *attr, const char *buf, size_t count) |
| { |
| int max_budget; |
| |
| if (kstrtoint(buf, 10, &max_budget) == 0) { |
| if (max_budget >= 0 && max_budget <= 50000) { |
| j_info.max_budget = max_budget; |
| j_info.budget = max_budget; |
| } else { |
| pr_info("[max_budget] should be 0 ~ 50000\n"); |
| } |
| return count; |
| } |
| |
| pr_info("[max_budget] invalid input\n"); |
| |
| return -EINVAL; |
| } |
| |
| static ssize_t interval_store(struct kobject *kobj, |
| struct kobj_attribute *attr, const char *buf, size_t count) |
| { |
| int interval; |
| |
| if (kstrtoint(buf, 10, &interval) == 0) { |
| if (interval >= 0 && interval <= 10000) |
| j_info.interval = interval; |
| else |
| pr_info("[interval] should be 0 ~ 10000\n"); |
| return count; |
| } |
| |
| pr_info("[interval] invalid input\n"); |
| |
| return -EINVAL; |
| } |
| |
| static ssize_t mode_store(struct kobject *kobj, |
| struct kobj_attribute *attr, const char *buf, size_t count) |
| { |
| int mode; |
| |
| if (kstrtoint(buf, 10, &mode) == 0) { |
| if (mode >= 0 && mode <= 1) |
| j_info.mode = mode; |
| else |
| pr_info("[mode] should be 0 ~ 1\n"); |
| return count; |
| } |
| |
| pr_info("[mode] invalid input\n"); |
| |
| return -EINVAL; |
| } |
| |
| |
| static struct kobj_attribute jatm_info_attr = __ATTR_RW(jatm_info); |
| static struct kobj_attribute turn_on_attr = __ATTR_RW(turn_on); |
| static struct kobj_attribute max_budget_attr = __ATTR_WO(max_budget); |
| static struct kobj_attribute interval_attr = __ATTR_WO(interval); |
| static struct kobj_attribute mode_attr = __ATTR_WO(mode); |
| |
| |
| static struct attribute *jatm_attrs[] = { |
| &jatm_info_attr.attr, |
| &turn_on_attr.attr, |
| &max_budget_attr.attr, |
| &interval_attr.attr, |
| &mode_attr.attr, |
| NULL |
| }; |
| static struct attribute_group jatm_attr_group = { |
| .name = "jatm", |
| .attrs = jatm_attrs, |
| }; |
| |
| static const struct of_device_id therm_jatm_of_match[] = { |
| { .compatible = "mediatek,therm_jatm", }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, therm_jatm_of_match); |
| |
| static int therm_jatm_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct cpufreq_policy *policy; |
| struct jatm_policy *j_policy; |
| int cpu, ret; |
| |
| if (!pdev->dev.of_node) { |
| dev_err(&pdev->dev, "Only DT based supported\n"); |
| return -ENODEV; |
| } |
| |
| for_each_possible_cpu(cpu) { |
| policy = cpufreq_cpu_get(cpu); |
| if (!policy) |
| continue; |
| |
| if (policy->cpu == cpu) { |
| j_policy = devm_kzalloc(dev, sizeof(*j_policy), GFP_KERNEL); |
| if (!j_policy) |
| return -ENOMEM; |
| |
| j_policy->policy = policy; |
| j_policy->cpu = cpu; |
| |
| ret = freq_qos_add_request(&policy->constraints, |
| &j_policy->qos_req, FREQ_QOS_MIN, |
| FREQ_QOS_MIN_DEFAULT_VALUE); |
| |
| if (ret < 0) { |
| dev_err(&pdev->dev, "%s: Fail to add freq constraint (%d)\n", |
| __func__, ret); |
| return ret; |
| } |
| list_add_tail(&j_policy->jatm_list, &jatm_policy_list); |
| |
| } |
| } |
| |
| ret = sysfs_create_group(kernel_kobj, &jatm_attr_group); |
| if (ret) { |
| dev_info(&pdev->dev, "failed to create thermal sysfs, ret=%d!\n", ret); |
| return ret; |
| } |
| |
| j_info.max_budget = DEFAULT_JATM_INFINITE_BUDGET; |
| j_info.budget = DEFAULT_JATM_INFINITE_BUDGET; |
| j_info.interval = DEFAULT_JATM_INTERVAL; |
| j_info.activated = false; |
| j_info.fix_opp = false; |
| /* jatm stop time out */ |
| j_info.stop_deadline = DEFAULT_JATM_STOP_DEADLINE; |
| |
| /* jatm_delay_start*/ |
| /* true : delay jatm enable after certain delay (allocate for heavier frame)*/ |
| /* false : enable just after */ |
| j_info.delay_start = false; |
| |
| j_info.mode = STOP_DEADLINE; |
| |
| jatm_notify_fp = jatm_notify_fp_cb; |
| |
| return 0; |
| } |
| |
| static int therm_jatm_remove(struct platform_device *pdev) |
| { |
| sysfs_remove_group(kernel_kobj, &jatm_attr_group); |
| |
| return 0; |
| } |
| |
| static struct platform_driver therm_jatm_driver = { |
| .probe = therm_jatm_probe, |
| .remove = therm_jatm_remove, |
| .driver = { |
| .name = "mtk-thermal-jatm", |
| .of_match_table = therm_jatm_of_match, |
| }, |
| }; |
| |
| module_platform_driver(therm_jatm_driver); |
| |
| MODULE_AUTHOR("Henry Huang <henry.huang@mediatek.com>"); |
| MODULE_DESCRIPTION("Mediatek jank aware thermal management driver"); |
| MODULE_LICENSE("GPL v2"); |