root/drivers/thermal/thermal_thresholds.c
// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright 2024 Linaro Limited
 *
 * Author: Daniel Lezcano <daniel.lezcano@linaro.org>
 *
 * Thermal thresholds
 */
#include <linux/list.h>
#include <linux/list_sort.h>
#include <linux/slab.h>

#include "thermal_core.h"
#include "thermal_thresholds.h"

int thermal_thresholds_init(struct thermal_zone_device *tz)
{
        INIT_LIST_HEAD(&tz->user_thresholds);

        return 0;
}

static void __thermal_thresholds_flush(struct thermal_zone_device *tz)
{
        struct list_head *thresholds = &tz->user_thresholds;
        struct user_threshold *entry, *tmp;

        list_for_each_entry_safe(entry, tmp, thresholds, list_node) {
                list_del(&entry->list_node);
                kfree(entry);
        }
}

void thermal_thresholds_flush(struct thermal_zone_device *tz)
{
        lockdep_assert_held(&tz->lock);

        __thermal_thresholds_flush(tz);

        thermal_notify_threshold_flush(tz);

        __thermal_zone_device_update(tz, THERMAL_TZ_FLUSH_THRESHOLDS);
}

void thermal_thresholds_exit(struct thermal_zone_device *tz)
{
        __thermal_thresholds_flush(tz);
}

static int __thermal_thresholds_cmp(void *data,
                                    const struct list_head *l1,
                                    const struct list_head *l2)
{
        struct user_threshold *t1 = container_of(l1, struct user_threshold, list_node);
        struct user_threshold *t2 = container_of(l2, struct user_threshold, list_node);

        return t1->temperature - t2->temperature;
}

static struct user_threshold *__thermal_thresholds_find(const struct list_head *thresholds,
                                                        int temperature)
{
        struct user_threshold *t;

        list_for_each_entry(t, thresholds, list_node)
                if (t->temperature == temperature)
                        return t;

        return NULL;
}

static bool thermal_thresholds_handle_raising(struct list_head *thresholds, int temperature,
                                              int last_temperature)
{
        struct user_threshold *t;

        list_for_each_entry(t, thresholds, list_node) {

                if (!(t->direction & THERMAL_THRESHOLD_WAY_UP))
                    continue;

                if (temperature >= t->temperature &&
                    last_temperature < t->temperature)
                        return true;
        }

        return false;
}

static bool thermal_thresholds_handle_dropping(struct list_head *thresholds, int temperature,
                                               int last_temperature)
{
        struct user_threshold *t;

        list_for_each_entry_reverse(t, thresholds, list_node) {

                if (!(t->direction & THERMAL_THRESHOLD_WAY_DOWN))
                    continue;

                if (temperature <= t->temperature &&
                    last_temperature > t->temperature)
                        return true;
        }

        return false;
}

static void thermal_threshold_find_boundaries(struct list_head *thresholds, int temperature,
                                              int *low, int *high)
{
        struct user_threshold *t;

        list_for_each_entry(t, thresholds, list_node) {
                if (temperature < t->temperature &&
                    (t->direction & THERMAL_THRESHOLD_WAY_UP) &&
                    *high > t->temperature)
                        *high = t->temperature;
        }

        list_for_each_entry_reverse(t, thresholds, list_node) {
                if (temperature > t->temperature &&
                    (t->direction & THERMAL_THRESHOLD_WAY_DOWN) &&
                    *low < t->temperature)
                        *low = t->temperature;
        }
}

void thermal_thresholds_handle(struct thermal_zone_device *tz, int *low, int *high)
{
        struct list_head *thresholds = &tz->user_thresholds;

        int temperature = tz->temperature;
        int last_temperature = tz->last_temperature;

        lockdep_assert_held(&tz->lock);

        thermal_threshold_find_boundaries(thresholds, temperature, low, high);

        /*
         * We need a second update in order to detect a threshold being crossed
         */
        if (last_temperature == THERMAL_TEMP_INVALID)
                return;

        /*
         * The temperature is stable, so obviously we can not have
         * crossed a threshold.
         */
        if (last_temperature == temperature)
                return;

        /*
         * Since last update the temperature:
         * - increased : thresholds are crossed the way up
         * - decreased : thresholds are crossed the way down
         */
        if (temperature > last_temperature) {
                if (thermal_thresholds_handle_raising(thresholds,
                                                      temperature, last_temperature))
                        thermal_notify_threshold_up(tz);
        } else {
                if (thermal_thresholds_handle_dropping(thresholds,
                                                       temperature, last_temperature))
                        thermal_notify_threshold_down(tz);
        }
}

int thermal_thresholds_add(struct thermal_zone_device *tz,
                           int temperature, int direction)
{
        struct list_head *thresholds = &tz->user_thresholds;
        struct user_threshold *t;

        lockdep_assert_held(&tz->lock);

        t = __thermal_thresholds_find(thresholds, temperature);
        if (t) {
                if (t->direction == direction)
                        return -EEXIST;

                t->direction |= direction;
        } else {

                t = kmalloc_obj(*t);
                if (!t)
                        return -ENOMEM;

                INIT_LIST_HEAD(&t->list_node);
                t->temperature = temperature;
                t->direction = direction;
                list_add(&t->list_node, thresholds);
                list_sort(NULL, thresholds, __thermal_thresholds_cmp);
        }

        thermal_notify_threshold_add(tz, temperature, direction);

        __thermal_zone_device_update(tz, THERMAL_TZ_ADD_THRESHOLD);

        return 0;
}

int thermal_thresholds_delete(struct thermal_zone_device *tz,
                              int temperature, int direction)
{
        struct list_head *thresholds = &tz->user_thresholds;
        struct user_threshold *t;

        lockdep_assert_held(&tz->lock);

        t = __thermal_thresholds_find(thresholds, temperature);
        if (!t)
                return -ENOENT;

        if (t->direction == direction) {
                list_del(&t->list_node);
                kfree(t);
        } else {
                t->direction &= ~direction;
        }

        thermal_notify_threshold_delete(tz, temperature, direction);

        __thermal_zone_device_update(tz, THERMAL_TZ_DEL_THRESHOLD);

        return 0;
}

int thermal_thresholds_for_each(struct thermal_zone_device *tz,
                                int (*cb)(struct user_threshold *, void *arg), void *arg)
{
        struct list_head *thresholds = &tz->user_thresholds;
        struct user_threshold *entry;
        int ret;

        guard(thermal_zone)(tz);

        list_for_each_entry(entry, thresholds, list_node) {
                ret = cb(entry, arg);
                if (ret)
                        return ret;
        }

        return 0;
}