blob: 1c5415afe687f5ea4a7da32f0614ad57b5de1237 [file] [log] [blame]
/* Copyright (c) 2016-2017, The Linux Foundation. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 and
* only version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#define pr_fmt(fmt) "%s:%s " fmt, KBUILD_MODNAME, __func__
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/thermal.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_irq.h>
#include <linux/platform_device.h>
#include <linux/sched.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/timer.h>
#include <linux/pm_opp.h>
#include <linux/cpu_cooling.h>
#include <linux/bitmap.h>
#include <linux/msm_thermal.h>
#include <asm/smp_plat.h>
#include <asm/cacheflush.h>
#include <soc/qcom/scm.h>
#include "thermal_core.h"
#define CREATE_TRACE_POINTS
#define LMH_DCVS_TRACE
#include <trace/trace_thermal.h>
#undef CREATE_TRACE_POINTS
#include <trace/events/power.h>
#define MSM_LIMITS_DCVSH 0x10
#define MSM_LIMITS_NODE_DCVS 0x44435653
#define MSM_LIMITS_SUB_FN_THERMAL 0x54484D4C
#define MSM_LIMITS_SUB_FN_GENERAL 0x47454E00
#define MSM_LIMITS_ALGO_MODE_ENABLE 0x454E424C
#define MSM_LIMITS_HI_THRESHOLD 0x48494748
#define MSM_LIMITS_LOW_THRESHOLD 0x4C4F5700
#define MSM_LIMITS_ARM_THRESHOLD 0x41524D00
#define MSM_LIMITS_CLUSTER_0 0x6370302D
#define MSM_LIMITS_CLUSTER_1 0x6370312D
#define MSM_LIMITS_DOMAIN_MAX 0x444D4158
#define MSM_LIMITS_HIGH_THRESHOLD_VAL 95000
#define MSM_LIMITS_ARM_THRESHOLD_VAL 65000
#define MSM_LIMITS_LOW_THRESHOLD_OFFSET 500
#define MSM_LIMITS_POLLING_DELAY_MS 10
#define MSM_LIMITS_CLUSTER_0_REQ 0x179C1B04
#define MSM_LIMITS_CLUSTER_1_REQ 0x179C3B04
#define MSM_LIMITS_CLUSTER_0_INT_CLR 0x179CE808
#define MSM_LIMITS_CLUSTER_1_INT_CLR 0x179CC808
#define dcvsh_get_frequency(_val, _max) do { \
_max = (_val) & 0x3FF; \
_max *= 19200; \
} while (0)
#define FREQ_KHZ_TO_HZ(_val) ((_val) * 1000)
#define FREQ_HZ_TO_KHZ(_val) ((_val) / 1000)
enum lmh_hw_trips {
LIMITS_TRIP_LO,
LIMITS_TRIP_HI,
LIMITS_TRIP_MAX,
};
struct msm_lmh_dcvs_hw {
char sensor_name[THERMAL_NAME_LENGTH];
uint32_t affinity;
uint32_t temp_limits[LIMITS_TRIP_MAX];
struct sensor_threshold default_lo, default_hi;
int irq_num;
void *osm_hw_reg;
void *int_clr_reg;
cpumask_t core_map;
struct timer_list poll_timer;
uint32_t max_freq;
uint32_t hw_freq_limit;
struct list_head list;
DECLARE_BITMAP(is_irq_enabled, 1);
};
LIST_HEAD(lmh_dcvs_hw_list);
static void msm_lmh_dcvs_get_max_freq(uint32_t cpu, uint32_t *max_freq)
{
unsigned long freq_ceil = UINT_MAX;
struct device *cpu_dev = NULL;
cpu_dev = get_cpu_device(cpu);
if (!cpu_dev) {
pr_err("Error in get CPU%d device\n", cpu);
return;
}
rcu_read_lock();
dev_pm_opp_find_freq_floor(cpu_dev, &freq_ceil);
rcu_read_unlock();
*max_freq = freq_ceil/1000;
}
static uint32_t msm_lmh_mitigation_notify(struct msm_lmh_dcvs_hw *hw)
{
uint32_t val = 0;
struct device *cpu_dev = NULL;
unsigned long freq_val, max_limit = 0;
struct dev_pm_opp *opp_entry;
val = readl_relaxed(hw->osm_hw_reg);
dcvsh_get_frequency(val, max_limit);
cpu_dev = get_cpu_device(cpumask_first(&hw->core_map));
if (!cpu_dev) {
pr_err("Error in get CPU%d device\n",
cpumask_first(&hw->core_map));
goto notify_exit;
}
freq_val = FREQ_KHZ_TO_HZ(max_limit);
rcu_read_lock();
opp_entry = dev_pm_opp_find_freq_floor(cpu_dev, &freq_val);
/*
* Hardware mitigation frequency can be lower than the lowest
* possible CPU frequency. In that case freq floor call will
* fail with -ERANGE and we need to match to the lowest
* frequency using freq_ceil.
*/
if (IS_ERR(opp_entry) && PTR_ERR(opp_entry) == -ERANGE) {
opp_entry = dev_pm_opp_find_freq_ceil(cpu_dev, &freq_val);
if (IS_ERR(opp_entry))
dev_err(cpu_dev, "frequency:%lu. opp error:%ld\n",
freq_val, PTR_ERR(opp_entry));
}
rcu_read_unlock();
max_limit = FREQ_HZ_TO_KHZ(freq_val);
trace_lmh_dcvs_freq(cpumask_first(&hw->core_map), max_limit);
trace_clock_set_rate(hw->sensor_name,
max_limit,
cpumask_first(&hw->core_map));
notify_exit:
hw->hw_freq_limit = max_limit;
return max_limit;
}
static void msm_lmh_dcvs_poll(unsigned long data)
{
uint32_t max_limit = 0;
struct msm_lmh_dcvs_hw *hw = (struct msm_lmh_dcvs_hw *)data;
if (hw->max_freq == UINT_MAX)
msm_lmh_dcvs_get_max_freq(cpumask_first(&hw->core_map),
&hw->max_freq);
max_limit = msm_lmh_mitigation_notify(hw);
if (max_limit >= hw->max_freq) {
del_timer(&hw->poll_timer);
writel_relaxed(0xFF, hw->int_clr_reg);
set_bit(1, hw->is_irq_enabled);
enable_irq(hw->irq_num);
} else {
mod_timer(&hw->poll_timer, jiffies + msecs_to_jiffies(
MSM_LIMITS_POLLING_DELAY_MS));
}
}
static void lmh_dcvs_notify(struct msm_lmh_dcvs_hw *hw)
{
if (test_and_clear_bit(1, hw->is_irq_enabled)) {
disable_irq_nosync(hw->irq_num);
msm_lmh_mitigation_notify(hw);
mod_timer(&hw->poll_timer, jiffies + msecs_to_jiffies(
MSM_LIMITS_POLLING_DELAY_MS));
}
}
static irqreturn_t lmh_dcvs_handle_isr(int irq, void *data)
{
struct msm_lmh_dcvs_hw *hw = data;
lmh_dcvs_notify(hw);
return IRQ_HANDLED;
}
static int msm_lmh_dcvs_write(uint32_t node_id, uint32_t fn,
uint32_t setting, uint32_t val)
{
int ret;
struct scm_desc desc_arg;
uint32_t *payload = NULL;
payload = kzalloc(sizeof(uint32_t) * 5, GFP_KERNEL);
if (!payload)
return -ENOMEM;
payload[0] = fn; /* algorithm */
payload[1] = 0; /* unused sub-algorithm */
payload[2] = setting;
payload[3] = 1; /* number of values */
payload[4] = val;
desc_arg.args[0] = SCM_BUFFER_PHYS(payload);
desc_arg.args[1] = sizeof(uint32_t) * 5;
desc_arg.args[2] = MSM_LIMITS_NODE_DCVS;
desc_arg.args[3] = node_id;
desc_arg.args[4] = 0; /* version */
desc_arg.arginfo = SCM_ARGS(5, SCM_RO, SCM_VAL, SCM_VAL,
SCM_VAL, SCM_VAL);
dmac_flush_range(payload, (void *)payload + 5 * (sizeof(uint32_t)));
ret = scm_call2(SCM_SIP_FNID(SCM_SVC_LMH, MSM_LIMITS_DCVSH), &desc_arg);
kfree(payload);
return ret;
}
static int lmh_get_trip_type(struct thermal_zone_device *dev,
int trip, enum thermal_trip_type *type)
{
switch (trip) {
case LIMITS_TRIP_LO:
*type = THERMAL_TRIP_CONFIGURABLE_LOW;
break;
case LIMITS_TRIP_HI:
*type = THERMAL_TRIP_CONFIGURABLE_HI;
break;
default:
return -EINVAL;
}
return 0;
}
static int lmh_activate_trip(struct thermal_zone_device *dev,
int trip, enum thermal_trip_activation_mode mode)
{
struct msm_lmh_dcvs_hw *hw = dev->devdata;
uint32_t enable, temp;
int ret = 0;
enable = (mode == THERMAL_TRIP_ACTIVATION_ENABLED) ? 1 : 0;
if (!enable) {
pr_info("%s: disable not supported\n", __func__);
return 0;
}
/* Sanity check limits before writing to the hardware */
if (hw->temp_limits[LIMITS_TRIP_LO] >=
hw->temp_limits[LIMITS_TRIP_HI])
return -EINVAL;
temp = hw->temp_limits[trip];
switch (trip) {
case LIMITS_TRIP_LO:
ret = msm_lmh_dcvs_write(hw->affinity,
MSM_LIMITS_SUB_FN_THERMAL,
MSM_LIMITS_ARM_THRESHOLD, temp);
break;
case LIMITS_TRIP_HI:
/*
* The high threshold should be atleast greater than the
* low threshold offset
*/
if (temp < MSM_LIMITS_LOW_THRESHOLD_OFFSET)
return -EINVAL;
ret = msm_lmh_dcvs_write(hw->affinity,
MSM_LIMITS_SUB_FN_THERMAL,
MSM_LIMITS_HI_THRESHOLD, temp);
if (ret)
break;
ret = msm_lmh_dcvs_write(hw->affinity,
MSM_LIMITS_SUB_FN_THERMAL,
MSM_LIMITS_LOW_THRESHOLD, temp -
MSM_LIMITS_LOW_THRESHOLD_OFFSET);
break;
default:
return -EINVAL;
}
return ret;
}
static int lmh_get_trip_temp(struct thermal_zone_device *dev,
int trip, int *value)
{
struct msm_lmh_dcvs_hw *hw = dev->devdata;
*value = hw->temp_limits[trip];
return 0;
}
static int lmh_set_trip_temp(struct thermal_zone_device *dev,
int trip, int value)
{
struct msm_lmh_dcvs_hw *hw = dev->devdata;
if (value < 0) {
pr_err("Value out of range :%d\n", value);
return -EINVAL;
}
hw->temp_limits[trip] = (uint32_t)value;
return 0;
}
static struct thermal_zone_device_ops limits_sensor_ops = {
.get_trip_type = lmh_get_trip_type,
.activate_trip_type = lmh_activate_trip,
.get_trip_temp = lmh_get_trip_temp,
.set_trip_temp = lmh_set_trip_temp,
};
static int trip_notify(enum thermal_trip_type type, int temp, void *data)
{
return 0;
}
static struct msm_lmh_dcvs_hw *get_dcvsh_hw_from_cpu(int cpu)
{
struct msm_lmh_dcvs_hw *hw;
list_for_each_entry(hw, &lmh_dcvs_hw_list, list) {
if (cpumask_test_cpu(cpu, &hw->core_map))
return hw;
}
return NULL;
}
static int lmh_set_max_limit(int cpu, u32 freq)
{
struct msm_lmh_dcvs_hw *hw = get_dcvsh_hw_from_cpu(cpu);
if (!hw)
return -EINVAL;
return msm_lmh_dcvs_write(hw->affinity, MSM_LIMITS_SUB_FN_GENERAL,
MSM_LIMITS_DOMAIN_MAX, freq);
}
static int lmh_get_cur_limit(int cpu, unsigned long *freq)
{
struct msm_lmh_dcvs_hw *hw = get_dcvsh_hw_from_cpu(cpu);
if (!hw)
return -EINVAL;
*freq = hw->hw_freq_limit;
return 0;
}
static struct cpu_cooling_ops cd_ops = {
.get_cur_state = lmh_get_cur_limit,
.ceil_limit = lmh_set_max_limit,
};
int msm_lmh_dcvsh_sw_notify(int cpu)
{
struct msm_lmh_dcvs_hw *hw = get_dcvsh_hw_from_cpu(cpu);
if (!hw)
return -EINVAL;
lmh_dcvs_notify(hw);
return 0;
}
static int msm_lmh_dcvs_probe(struct platform_device *pdev)
{
int ret;
int affinity = -1;
struct msm_lmh_dcvs_hw *hw;
struct thermal_zone_device *tzdev;
struct thermal_cooling_device *cdev;
struct device_node *dn = pdev->dev.of_node;
struct device_node *cpu_node, *lmh_node;
uint32_t id, max_freq, request_reg, clear_reg;
int cpu;
cpumask_t mask = { CPU_BITS_NONE };
for_each_possible_cpu(cpu) {
cpu_node = of_cpu_device_node_get(cpu);
if (!cpu_node)
continue;
lmh_node = of_parse_phandle(cpu_node, "qcom,lmh-dcvs", 0);
if (lmh_node == dn) {
affinity = MPIDR_AFFINITY_LEVEL(
cpu_logical_map(cpu), 1);
/*set the cpumask*/
cpumask_set_cpu(cpu, &(mask));
}
of_node_put(cpu_node);
of_node_put(lmh_node);
}
/*
* We return error if none of the CPUs have
* reference to our LMH node
*/
if (affinity == -1)
return -EINVAL;
msm_lmh_dcvs_get_max_freq(cpumask_first(&mask), &max_freq);
hw = devm_kzalloc(&pdev->dev, sizeof(*hw), GFP_KERNEL);
if (!hw)
return -ENOMEM;
cpumask_copy(&hw->core_map, &mask);
switch (affinity) {
case 0:
hw->affinity = MSM_LIMITS_CLUSTER_0;
break;
case 1:
hw->affinity = MSM_LIMITS_CLUSTER_1;
break;
default:
return -EINVAL;
};
/* Enable the thermal algorithm early */
ret = msm_lmh_dcvs_write(hw->affinity, MSM_LIMITS_SUB_FN_THERMAL,
MSM_LIMITS_ALGO_MODE_ENABLE, 1);
if (ret)
return ret;
hw->default_lo.temp = MSM_LIMITS_ARM_THRESHOLD_VAL;
hw->default_lo.trip = THERMAL_TRIP_CONFIGURABLE_LOW;
hw->default_lo.notify = trip_notify;
hw->default_hi.temp = MSM_LIMITS_HIGH_THRESHOLD_VAL;
hw->default_hi.trip = THERMAL_TRIP_CONFIGURABLE_HI;
hw->default_hi.notify = trip_notify;
/*
* Setup virtual thermal zones for each LMH-DCVS hardware
* The sensor does not do actual thermal temperature readings
* but does support setting thresholds for trips.
* Let's register with thermal framework, so we have the ability
* to set low/high thresholds.
*/
snprintf(hw->sensor_name, sizeof(hw->sensor_name), "limits_sensor-%02d",
affinity);
tzdev = thermal_zone_device_register(hw->sensor_name, LIMITS_TRIP_MAX,
(1 << LIMITS_TRIP_MAX) - 1, hw, &limits_sensor_ops,
NULL, 0, 0);
if (IS_ERR_OR_NULL(tzdev))
return PTR_ERR(tzdev);
/* Setup cooling devices to request mitigation states */
cdev = cpufreq_platform_cooling_register(&hw->core_map, &cd_ops);
if (IS_ERR_OR_NULL(cdev))
return PTR_ERR(cdev);
/*
* Driver defaults to for low and hi thresholds.
* Since we make a check for hi > lo value, set the hi threshold
* before the low threshold
*/
id = sensor_get_id(hw->sensor_name);
if (id < 0)
return id;
ret = sensor_set_trip(id, &hw->default_hi);
if (!ret) {
ret = sensor_activate_trip(id, &hw->default_hi, true);
if (ret)
return ret;
} else {
return ret;
}
ret = sensor_set_trip(id, &hw->default_lo);
if (!ret) {
ret = sensor_activate_trip(id, &hw->default_lo, true);
if (ret)
return ret;
}
hw->hw_freq_limit = hw->max_freq = max_freq;
switch (affinity) {
case 0:
request_reg = MSM_LIMITS_CLUSTER_0_REQ;
clear_reg = MSM_LIMITS_CLUSTER_0_INT_CLR;
break;
case 1:
request_reg = MSM_LIMITS_CLUSTER_1_REQ;
clear_reg = MSM_LIMITS_CLUSTER_1_INT_CLR;
break;
default:
return -EINVAL;
};
hw->osm_hw_reg = devm_ioremap(&pdev->dev, request_reg, 0x4);
if (!hw->osm_hw_reg) {
pr_err("register remap failed\n");
return -ENOMEM;
}
hw->int_clr_reg = devm_ioremap(&pdev->dev, clear_reg, 0x4);
if (!hw->int_clr_reg) {
pr_err("interrupt clear reg remap failed\n");
return -ENOMEM;
}
init_timer_deferrable(&hw->poll_timer);
hw->poll_timer.data = (unsigned long)hw;
hw->poll_timer.function = msm_lmh_dcvs_poll;
hw->irq_num = of_irq_get(pdev->dev.of_node, 0);
if (hw->irq_num < 0) {
ret = hw->irq_num;
pr_err("Error getting IRQ number. err:%d\n", ret);
return ret;
}
set_bit(1, hw->is_irq_enabled);
ret = devm_request_threaded_irq(&pdev->dev, hw->irq_num, NULL,
lmh_dcvs_handle_isr, IRQF_TRIGGER_HIGH | IRQF_ONESHOT
| IRQF_NO_SUSPEND, hw->sensor_name, hw);
if (ret) {
pr_err("Error registering for irq. err:%d\n", ret);
return ret;
}
INIT_LIST_HEAD(&hw->list);
list_add(&hw->list, &lmh_dcvs_hw_list);
return ret;
}
static const struct of_device_id msm_lmh_dcvs_match[] = {
{ .compatible = "qcom,msm-hw-limits", },
{},
};
static struct platform_driver msm_lmh_dcvs_driver = {
.probe = msm_lmh_dcvs_probe,
.driver = {
.name = KBUILD_MODNAME,
.of_match_table = msm_lmh_dcvs_match,
},
};
builtin_platform_driver(msm_lmh_dcvs_driver);