| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Thermal management support for GCIP devices. |
| * |
| * Copyright (C) 2023 Google LLC |
| */ |
| |
| #include <linux/debugfs.h> |
| #include <linux/device.h> |
| #include <linux/minmax.h> |
| #include <linux/mutex.h> |
| #include <linux/notifier.h> |
| #include <linux/thermal.h> |
| #include <linux/version.h> |
| |
| #include <gcip/gcip-config.h> |
| #include <gcip/gcip-pm.h> |
| #include <gcip/gcip-thermal.h> |
| |
| #define OF_DATA_NUM_MAX (GCIP_THERMAL_MAX_NUM_STATES * 2) |
| |
| #define to_cdev(dev) container_of(dev, struct thermal_cooling_device, device) |
| #define to_gcip_thermal(dev) ((struct gcip_thermal *)to_cdev(dev)->devdata) |
| |
| /* Struct for state to rate and state to power mappings. */ |
| struct gcip_rate_pwr { |
| unsigned long rate; |
| u32 power; |
| }; |
| |
| static struct gcip_rate_pwr state_map[GCIP_THERMAL_MAX_NUM_STATES] = { 0 }; |
| |
| static int gcip_thermal_get_max_state(struct thermal_cooling_device *cdev, unsigned long *state) |
| { |
| struct gcip_thermal *thermal = cdev->devdata; |
| |
| if (!thermal->num_states) |
| return -ENODEV; |
| |
| *state = thermal->num_states - 1; |
| |
| return 0; |
| } |
| |
| static int gcip_thermal_get_cur_state(struct thermal_cooling_device *cdev, unsigned long *state) |
| { |
| struct gcip_thermal *thermal = cdev->devdata; |
| |
| mutex_lock(&thermal->lock); |
| *state = thermal->state; |
| mutex_unlock(&thermal->lock); |
| |
| return 0; |
| } |
| |
| static int gcip_thermal_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state) |
| { |
| struct gcip_thermal *thermal = cdev->devdata; |
| int i, ret = 0; |
| |
| if (state >= thermal->num_states) { |
| dev_err(thermal->dev, "Invalid thermal cooling state %lu\n", state); |
| return -EINVAL; |
| } |
| |
| mutex_lock(&thermal->lock); |
| |
| thermal->vote[GCIP_THERMAL_COOLING_DEVICE] = state; |
| for (i = 0; i < GCIP_THERMAL_MAX_NUM_VOTERS; i++) |
| state = max(state, thermal->vote[i]); |
| |
| if (state == thermal->state) |
| goto out; |
| |
| if (!gcip_pm_get_if_powered(thermal->pm, false)) { |
| ret = thermal->set_rate(thermal->data, state_map[state].rate); |
| gcip_pm_put(thermal->pm); |
| } |
| |
| if (ret) |
| dev_err(thermal->dev, "Failed to set thermal cooling state: %d\n", ret); |
| else |
| thermal->state = state; |
| out: |
| mutex_unlock(&thermal->lock); |
| |
| return ret; |
| } |
| |
| static int gcip_thermal_rate2power_internal(struct gcip_thermal *thermal, unsigned long rate, |
| u32 *power) |
| { |
| int i; |
| |
| for (i = 0; i < thermal->num_states; i++) { |
| if (rate == state_map[i].rate) { |
| *power = state_map[i].power; |
| return 0; |
| } |
| } |
| |
| dev_err(thermal->dev, "Unknown rate for: %lu\n", rate); |
| *power = 0; |
| |
| return -EINVAL; |
| } |
| |
| static int gcip_thermal_get_requested_power(struct thermal_cooling_device *cdev, u32 *power) |
| { |
| struct gcip_thermal *thermal = cdev->devdata; |
| unsigned long rate; |
| int ret; |
| |
| if (gcip_pm_get_if_powered(thermal->pm, false)) { |
| *power = 0; |
| return 0; |
| } |
| |
| mutex_lock(&thermal->lock); |
| |
| ret = thermal->get_rate(thermal->data, &rate); |
| |
| mutex_unlock(&thermal->lock); |
| gcip_pm_put(thermal->pm); |
| |
| if (ret) |
| return ret; |
| |
| return gcip_thermal_rate2power_internal(thermal, rate, power); |
| } |
| |
| static int gcip_thermal_state2power(struct thermal_cooling_device *cdev, unsigned long state, |
| u32 *power) |
| { |
| struct gcip_thermal *thermal = cdev->devdata; |
| |
| if (state >= thermal->num_states) { |
| dev_err(thermal->dev, "Invalid state: %lu\n", state); |
| return -EINVAL; |
| } |
| |
| return gcip_thermal_rate2power_internal(thermal, state_map[state].rate, power); |
| } |
| |
| static int gcip_thermal_power2state(struct thermal_cooling_device *cdev, u32 power, |
| unsigned long *state) |
| { |
| struct gcip_thermal *thermal = cdev->devdata; |
| |
| if (!thermal->num_states) |
| return -ENODEV; |
| |
| /* |
| * Argument "power" is the maximum allowed power consumption in mW as defined by the PID |
| * control loop. Checks for the first state that is less than or equal to the current |
| * allowed power. state_map is descending, so lowest power consumption is last value in the |
| * array. Returns lowest state even if it consumes more power than allowed as not all |
| * platforms can handle throttling below an active state. |
| */ |
| for (*state = 0; *state < thermal->num_states; (*state)++) |
| if (power >= state_map[*state].power) |
| return 0; |
| |
| *state = thermal->num_states - 1; |
| |
| return 0; |
| } |
| |
| static const struct thermal_cooling_device_ops gcip_thermal_ops = { |
| .get_max_state = gcip_thermal_get_max_state, |
| .get_cur_state = gcip_thermal_get_cur_state, |
| .set_cur_state = gcip_thermal_set_cur_state, |
| .get_requested_power = gcip_thermal_get_requested_power, |
| .state2power = gcip_thermal_state2power, |
| .power2state = gcip_thermal_power2state, |
| }; |
| |
| /* This API was removed, but Android still uses it to update thermal request. */ |
| #if GCIP_IS_GKI |
| void thermal_cdev_update(struct thermal_cooling_device *cdev); |
| #endif |
| |
| static void gcip_thermal_update(struct gcip_thermal *thermal) |
| { |
| struct thermal_cooling_device *cdev = thermal->cdev; |
| |
| cdev->updated = false; |
| |
| #if GCIP_IS_GKI |
| thermal_cdev_update(cdev); |
| #elif IS_ENABLED(CONFIG_THERMAL) |
| dev_err_once(thermal->dev, "Thermal update not implemented"); |
| #endif |
| } |
| |
| static ssize_t user_vote_show(struct device *dev, struct device_attribute *attr, char *buf) |
| { |
| struct gcip_thermal *thermal = to_gcip_thermal(dev); |
| ssize_t ret; |
| |
| if (!thermal) |
| return -ENODEV; |
| |
| mutex_lock(&thermal->lock); |
| ret = sysfs_emit(buf, "%lu\n", thermal->vote[GCIP_THERMAL_SYSFS]); |
| mutex_unlock(&thermal->lock); |
| |
| return ret; |
| } |
| |
| static ssize_t user_vote_store(struct device *dev, struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct gcip_thermal *thermal = to_gcip_thermal(dev); |
| unsigned long state; |
| int ret; |
| |
| if (!thermal) |
| return -ENODEV; |
| |
| ret = kstrtoul(buf, 0, &state); |
| if (ret) |
| return ret; |
| |
| if (state >= thermal->num_states) |
| return -EINVAL; |
| |
| mutex_lock(&thermal->lock); |
| thermal->vote[GCIP_THERMAL_SYSFS] = state; |
| mutex_unlock(&thermal->lock); |
| |
| gcip_thermal_update(thermal); |
| |
| return count; |
| } |
| |
| static DEVICE_ATTR_RW(user_vote); |
| |
| static int gcip_thermal_rate2state(struct gcip_thermal *thermal, unsigned long rate) |
| { |
| int i; |
| |
| for (i = 0; i < thermal->num_states; i++) { |
| if (state_map[i].rate <= rate) |
| return i; |
| } |
| |
| /* Returns lowest state on an invalid input. */ |
| return thermal->num_states - 1; |
| } |
| |
| static int gcip_thermal_notifier(struct notifier_block *nb, unsigned long rate, void *nb_data) |
| { |
| struct gcip_thermal *thermal = container_of(nb, struct gcip_thermal, nb); |
| unsigned long state = gcip_thermal_rate2state(thermal, rate); |
| |
| dev_dbg(thermal->dev, "Thermal notifier req original: %lu, state: %lu\n", rate, state); |
| |
| mutex_lock(&thermal->lock); |
| thermal->vote[GCIP_THERMAL_NOTIFIER_BLOCK] = state; |
| mutex_unlock(&thermal->lock); |
| |
| gcip_thermal_update(thermal); |
| |
| return NOTIFY_OK; |
| } |
| |
| struct notifier_block *gcip_thermal_get_notifier_block(struct gcip_thermal *thermal) |
| { |
| if (IS_ERR_OR_NULL(thermal)) |
| return NULL; |
| |
| return &thermal->nb; |
| } |
| |
| static ssize_t state2power_table_show(struct device *dev, struct device_attribute *attr, char *buf) |
| { |
| struct gcip_thermal *thermal = to_gcip_thermal(dev); |
| ssize_t count = 0; |
| int i; |
| u32 power; |
| |
| for (i = 0; i < thermal->num_states; i++) { |
| gcip_thermal_state2power(thermal->cdev, i, &power); |
| count += sysfs_emit_at(buf, count, "%u ", power); |
| } |
| count += sysfs_emit_at(buf, count, "\n"); |
| |
| return count; |
| } |
| |
| static DEVICE_ATTR_RO(state2power_table); |
| |
| void gcip_thermal_destroy(struct gcip_thermal *thermal) |
| { |
| if (IS_ERR_OR_NULL(thermal)) |
| return; |
| |
| debugfs_remove_recursive(thermal->dentry); |
| thermal_cooling_device_unregister(thermal->cdev); |
| devm_kfree(thermal->dev, thermal); |
| } |
| |
| static int gcip_thermal_enable_get(void *data, u64 *val) |
| { |
| struct gcip_thermal *thermal = (struct gcip_thermal *)data; |
| |
| mutex_lock(&thermal->lock); |
| *val = thermal->enabled; |
| mutex_unlock(&thermal->lock); |
| |
| return 0; |
| } |
| |
| static int gcip_thermal_enable_set(void *data, u64 val) |
| { |
| struct gcip_thermal *thermal = (struct gcip_thermal *)data; |
| int ret = 0; |
| |
| mutex_lock(&thermal->lock); |
| |
| if (thermal->enabled != (bool)val) { |
| /* |
| * If the device is not powered, the value will be restored by |
| * gcip_thermal_restore_on_powering in next fw boot. |
| */ |
| if (!gcip_pm_get_if_powered(thermal->pm, false)) { |
| ret = thermal->control(thermal->data, val); |
| gcip_pm_put(thermal->pm); |
| } |
| |
| if (!ret) { |
| thermal->enabled = val; |
| dev_info_ratelimited(thermal->dev, "%s thermal control", |
| thermal->enabled ? "Enable" : "Disable"); |
| } else { |
| dev_err(thermal->dev, "Failed to %s thermal control: %d ", |
| val ? "enable" : "disable", ret); |
| } |
| } |
| |
| mutex_unlock(&thermal->lock); |
| |
| return ret; |
| } |
| |
| DEFINE_DEBUGFS_ATTRIBUTE(fops_gcip_thermal_enable, gcip_thermal_enable_get, gcip_thermal_enable_set, |
| "%llu\n"); |
| |
| static int gcip_thermal_parse_dvfs_table(struct gcip_thermal *thermal) |
| { |
| int row_size, col_size, tbl_size, i; |
| int of_data_int_array[OF_DATA_NUM_MAX]; |
| |
| if (of_property_read_u32_array(thermal->dev->of_node, GCIP_THERMAL_TABLE_SIZE_NAME, |
| of_data_int_array, 2)) |
| goto error; |
| |
| row_size = of_data_int_array[0]; |
| col_size = of_data_int_array[1]; |
| tbl_size = row_size * col_size; |
| if (row_size > GCIP_THERMAL_MAX_NUM_STATES) { |
| dev_err(thermal->dev, "Too many states\n"); |
| goto error; |
| } |
| |
| if (tbl_size > OF_DATA_NUM_MAX) |
| goto error; |
| |
| if (of_property_read_u32_array(thermal->dev->of_node, GCIP_THERMAL_TABLE_NAME, |
| of_data_int_array, tbl_size)) |
| goto error; |
| |
| thermal->num_states = row_size; |
| for (i = 0; i < row_size; ++i) { |
| int idx = col_size * i; |
| |
| state_map[i].rate = of_data_int_array[idx]; |
| state_map[i].power = of_data_int_array[idx + 1]; |
| } |
| |
| return 0; |
| |
| error: |
| dev_err(thermal->dev, "Failed to parse DVFS table\n"); |
| |
| return -EINVAL; |
| } |
| |
| static int gcip_thermal_cooling_register(struct gcip_thermal *thermal, const char *type, |
| const char *node_name) |
| { |
| struct device_node *node = NULL; |
| int ret; |
| |
| ret = gcip_thermal_parse_dvfs_table(thermal); |
| if (ret) |
| return ret; |
| |
| if (node_name) |
| node = of_find_node_by_name(NULL, node_name); |
| if (!node) |
| dev_warn(thermal->dev, "Failed to find thermal cooling node\n"); |
| |
| thermal->cdev = thermal_of_cooling_device_register(node, type, thermal, &gcip_thermal_ops); |
| if (IS_ERR(thermal->cdev)) |
| return PTR_ERR(thermal->cdev); |
| |
| ret = device_create_file(&thermal->cdev->device, &dev_attr_user_vote); |
| if (ret) |
| thermal_cooling_device_unregister(thermal->cdev); |
| |
| ret = device_create_file(&thermal->cdev->device, &dev_attr_state2power_table); |
| if (ret) |
| thermal_cooling_device_unregister(thermal->cdev); |
| |
| return ret; |
| } |
| |
| struct gcip_thermal *gcip_thermal_create(const struct gcip_thermal_args *args) |
| { |
| struct gcip_thermal *thermal; |
| int ret; |
| |
| if (!args->dev || !args->get_rate || !args->set_rate || !args->control) |
| return ERR_PTR(-EINVAL); |
| |
| thermal = devm_kzalloc(args->dev, sizeof(*thermal), GFP_KERNEL); |
| if (!thermal) |
| return ERR_PTR(-ENOMEM); |
| |
| thermal->dev = args->dev; |
| thermal->nb.notifier_call = gcip_thermal_notifier; |
| thermal->pm = args->pm; |
| thermal->enabled = true; |
| thermal->data = args->data; |
| thermal->get_rate = args->get_rate; |
| thermal->set_rate = args->set_rate; |
| thermal->control = args->control; |
| |
| mutex_init(&thermal->lock); |
| |
| ret = gcip_thermal_cooling_register(thermal, args->type, args->node_name); |
| if (ret) { |
| dev_err(args->dev, "Failed to initialize external thermal cooling\n"); |
| devm_kfree(args->dev, thermal); |
| return ERR_PTR(ret); |
| } |
| |
| thermal->dentry = debugfs_create_dir("cooling", args->dentry); |
| /* Don't let debugfs creation failure abort the init procedure. */ |
| if (IS_ERR_OR_NULL(thermal->dentry)) |
| dev_warn(args->dev, "Failed to create debugfs for thermal cooling"); |
| else |
| debugfs_create_file("enable", 0660, thermal->dentry, thermal, |
| &fops_gcip_thermal_enable); |
| |
| return thermal; |
| } |
| |
| int gcip_thermal_suspend_device(struct gcip_thermal *thermal) |
| { |
| int ret = 0; |
| |
| if (IS_ERR_OR_NULL(thermal)) |
| return 0; |
| |
| mutex_lock(&thermal->lock); |
| |
| /* |
| * Always sets as suspended even when the request cannot be handled for unknown reasons |
| * because we still want to prevent the client from using device. |
| */ |
| thermal->device_suspended = true; |
| if (!gcip_pm_get_if_powered(thermal->pm, false)) { |
| ret = thermal->set_rate(thermal->data, 0); |
| gcip_pm_put(thermal->pm); |
| } |
| |
| mutex_unlock(&thermal->lock); |
| |
| return ret; |
| } |
| |
| int gcip_thermal_resume_device(struct gcip_thermal *thermal) |
| { |
| int ret = 0; |
| |
| if (IS_ERR_OR_NULL(thermal)) |
| return 0; |
| |
| mutex_lock(&thermal->lock); |
| |
| if (!gcip_pm_get_if_powered(thermal->pm, false)) { |
| ret = thermal->set_rate(thermal->data, state_map[thermal->state].rate); |
| gcip_pm_put(thermal->pm); |
| } |
| |
| /* |
| * Unlike gcip_thermal_suspend_device(), only sets the device as resumed if the request is |
| * fulfilled. |
| */ |
| if (!ret) |
| thermal->device_suspended = false; |
| |
| mutex_unlock(&thermal->lock); |
| |
| return ret; |
| } |
| |
| bool gcip_thermal_is_device_suspended(struct gcip_thermal *thermal) |
| { |
| if (IS_ERR_OR_NULL(thermal)) |
| return false; |
| |
| return thermal->device_suspended; |
| } |
| |
| int gcip_thermal_restore_on_powering(struct gcip_thermal *thermal) |
| { |
| int ret = 0; |
| |
| if (IS_ERR_OR_NULL(thermal)) |
| return 0; |
| |
| mutex_lock(&thermal->lock); |
| |
| if (!thermal->enabled) |
| ret = thermal->control(thermal->data, thermal->enabled); |
| else if (thermal->device_suspended) |
| ret = thermal->set_rate(thermal->data, 0); |
| else if (thermal->state) |
| /* Skips state 0 since it's the default thermal state. */ |
| ret = thermal->set_rate(thermal->data, state_map[thermal->state].rate); |
| |
| mutex_unlock(&thermal->lock); |
| |
| return ret; |
| } |