mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2025-01-06 13:16:22 +00:00
f6a034f2df
Introduce a facility allowing the thermal core functionality to be exercised in a controlled way in order to verify its behavior, without affecting its regular users noticeably. It is based on the idea of preparing thermal zone templates along with their trip points by writing to files in debugfs. When ready, those templates can be used for registering test thermal zones with the thermal core. The temperature of a test thermal zone created this way can be adjusted via debugfs, which also triggers a __thermal_zone_device_update() call for it. By manipulating the temperature of a test thermal zone, one can check if the thermal core reacts to the changes of it as expected. Signed-off-by: Rafael J. Wysocki <rafael.j.wysocki@intel.com> Link: https://patch.msgid.link/6065927.lOV4Wx5bFT@rjwysocki.net [ rjw: Fixed ordering of kcalloc() arguments ] [ rjw: Fixed debugfs_create_dir() return value checks ] [ rjw: Fixed two kerneldoc comments ] Signed-off-by: Rafael J. Wysocki <rafael.j.wysocki@intel.com>
469 lines
11 KiB
C
469 lines
11 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Copyright 2024, Intel Corporation
|
|
*
|
|
* Author: Rafael J. Wysocki <rafael.j.wysocki@intel.com>
|
|
*
|
|
* Thermal zone tempalates handling for thermal core testing.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) "thermal-testing: " fmt
|
|
|
|
#include <linux/debugfs.h>
|
|
#include <linux/idr.h>
|
|
#include <linux/list.h>
|
|
#include <linux/thermal.h>
|
|
#include <linux/workqueue.h>
|
|
|
|
#include "thermal_testing.h"
|
|
|
|
#define TT_MAX_FILE_NAME_LENGTH 16
|
|
|
|
/**
|
|
* struct tt_thermal_zone - Testing thermal zone template
|
|
*
|
|
* Represents a template of a thermal zone that can be used for registering
|
|
* a test thermal zone with the thermal core.
|
|
*
|
|
* @list_node: Node in the list of all testing thermal zone templates.
|
|
* @trips: List of trip point templates for this thermal zone template.
|
|
* @d_tt_zone: Directory in debugfs representing this template.
|
|
* @tz: Test thermal zone based on this template, if present.
|
|
* @lock: Mutex for synchronizing changes of this template.
|
|
* @ida: IDA for trip point IDs.
|
|
* @id: The ID of this template for the debugfs interface.
|
|
* @temp: Temperature value.
|
|
* @tz_temp: Current thermal zone temperature (after registration).
|
|
* @num_trips: Number of trip points in the @trips list.
|
|
* @refcount: Reference counter for usage and removal synchronization.
|
|
*/
|
|
struct tt_thermal_zone {
|
|
struct list_head list_node;
|
|
struct list_head trips;
|
|
struct dentry *d_tt_zone;
|
|
struct thermal_zone_device *tz;
|
|
struct mutex lock;
|
|
struct ida ida;
|
|
int id;
|
|
int temp;
|
|
int tz_temp;
|
|
unsigned int num_trips;
|
|
unsigned int refcount;
|
|
};
|
|
|
|
DEFINE_GUARD(tt_zone, struct tt_thermal_zone *, mutex_lock(&_T->lock), mutex_unlock(&_T->lock))
|
|
|
|
/**
|
|
* struct tt_trip - Testing trip point template
|
|
*
|
|
* Represents a template of a trip point to be used for populating a trip point
|
|
* during the registration of a thermal zone based on a given zone template.
|
|
*
|
|
* @list_node: Node in the list of all trip templates in the zone template.
|
|
* @trip: Trip point data to use for thernal zone registration.
|
|
* @id: The ID of this trip template for the debugfs interface.
|
|
*/
|
|
struct tt_trip {
|
|
struct list_head list_node;
|
|
struct thermal_trip trip;
|
|
int id;
|
|
};
|
|
|
|
/*
|
|
* It is both questionable and potentially problematic from the sychnronization
|
|
* perspective to attempt to manipulate debugfs from within a debugfs file
|
|
* "write" operation, so auxiliary work items are used for that. The majority
|
|
* of zone-related command functions have a part that runs from a workqueue and
|
|
* make changes in debugs, among other things.
|
|
*/
|
|
struct tt_work {
|
|
struct work_struct work;
|
|
struct tt_thermal_zone *tt_zone;
|
|
struct tt_trip *tt_trip;
|
|
};
|
|
|
|
static inline struct tt_work *tt_work_of_work(struct work_struct *work)
|
|
{
|
|
return container_of(work, struct tt_work, work);
|
|
}
|
|
|
|
static LIST_HEAD(tt_thermal_zones);
|
|
static DEFINE_IDA(tt_thermal_zones_ida);
|
|
static DEFINE_MUTEX(tt_thermal_zones_lock);
|
|
|
|
static int tt_int_get(void *data, u64 *val)
|
|
{
|
|
*val = *(int *)data;
|
|
return 0;
|
|
}
|
|
static int tt_int_set(void *data, u64 val)
|
|
{
|
|
if ((int)val < THERMAL_TEMP_INVALID)
|
|
return -EINVAL;
|
|
|
|
*(int *)data = val;
|
|
return 0;
|
|
}
|
|
DEFINE_DEBUGFS_ATTRIBUTE_SIGNED(tt_int_attr, tt_int_get, tt_int_set, "%lld\n");
|
|
DEFINE_DEBUGFS_ATTRIBUTE(tt_unsigned_int_attr, tt_int_get, tt_int_set, "%llu\n");
|
|
|
|
static int tt_zone_tz_temp_get(void *data, u64 *val)
|
|
{
|
|
struct tt_thermal_zone *tt_zone = data;
|
|
|
|
guard(tt_zone)(tt_zone);
|
|
|
|
if (!tt_zone->tz)
|
|
return -EBUSY;
|
|
|
|
*val = tt_zone->tz_temp;
|
|
|
|
return 0;
|
|
}
|
|
static int tt_zone_tz_temp_set(void *data, u64 val)
|
|
{
|
|
struct tt_thermal_zone *tt_zone = data;
|
|
|
|
guard(tt_zone)(tt_zone);
|
|
|
|
if (!tt_zone->tz)
|
|
return -EBUSY;
|
|
|
|
WRITE_ONCE(tt_zone->tz_temp, val);
|
|
thermal_zone_device_update(tt_zone->tz, THERMAL_EVENT_TEMP_SAMPLE);
|
|
|
|
return 0;
|
|
}
|
|
DEFINE_DEBUGFS_ATTRIBUTE_SIGNED(tt_zone_tz_temp_attr, tt_zone_tz_temp_get,
|
|
tt_zone_tz_temp_set, "%lld\n");
|
|
|
|
static void tt_zone_free_trips(struct tt_thermal_zone *tt_zone)
|
|
{
|
|
struct tt_trip *tt_trip, *aux;
|
|
|
|
list_for_each_entry_safe(tt_trip, aux, &tt_zone->trips, list_node) {
|
|
list_del(&tt_trip->list_node);
|
|
ida_free(&tt_zone->ida, tt_trip->id);
|
|
kfree(tt_trip);
|
|
}
|
|
}
|
|
|
|
static void tt_zone_free(struct tt_thermal_zone *tt_zone)
|
|
{
|
|
tt_zone_free_trips(tt_zone);
|
|
ida_free(&tt_thermal_zones_ida, tt_zone->id);
|
|
ida_destroy(&tt_zone->ida);
|
|
kfree(tt_zone);
|
|
}
|
|
|
|
static void tt_add_tz_work_fn(struct work_struct *work)
|
|
{
|
|
struct tt_work *tt_work = tt_work_of_work(work);
|
|
struct tt_thermal_zone *tt_zone = tt_work->tt_zone;
|
|
char f_name[TT_MAX_FILE_NAME_LENGTH];
|
|
|
|
kfree(tt_work);
|
|
|
|
snprintf(f_name, TT_MAX_FILE_NAME_LENGTH, "tz%d", tt_zone->id);
|
|
tt_zone->d_tt_zone = debugfs_create_dir(f_name, d_testing);
|
|
if (IS_ERR(tt_zone->d_tt_zone)) {
|
|
tt_zone_free(tt_zone);
|
|
return;
|
|
}
|
|
|
|
debugfs_create_file_unsafe("temp", 0600, tt_zone->d_tt_zone, tt_zone,
|
|
&tt_zone_tz_temp_attr);
|
|
|
|
debugfs_create_file_unsafe("init_temp", 0600, tt_zone->d_tt_zone,
|
|
&tt_zone->temp, &tt_int_attr);
|
|
|
|
guard(mutex)(&tt_thermal_zones_lock);
|
|
|
|
list_add_tail(&tt_zone->list_node, &tt_thermal_zones);
|
|
}
|
|
|
|
int tt_add_tz(void)
|
|
{
|
|
struct tt_thermal_zone *tt_zone __free(kfree);
|
|
struct tt_work *tt_work __free(kfree);
|
|
int ret;
|
|
|
|
tt_zone = kzalloc(sizeof(*tt_zone), GFP_KERNEL);
|
|
if (!tt_zone)
|
|
return -ENOMEM;
|
|
|
|
tt_work = kzalloc(sizeof(*tt_work), GFP_KERNEL);
|
|
if (!tt_work)
|
|
return -ENOMEM;
|
|
|
|
INIT_LIST_HEAD(&tt_zone->trips);
|
|
mutex_init(&tt_zone->lock);
|
|
ida_init(&tt_zone->ida);
|
|
tt_zone->temp = THERMAL_TEMP_INVALID;
|
|
|
|
ret = ida_alloc(&tt_thermal_zones_ida, GFP_KERNEL);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
tt_zone->id = ret;
|
|
|
|
INIT_WORK(&tt_work->work, tt_add_tz_work_fn);
|
|
tt_work->tt_zone = no_free_ptr(tt_zone);
|
|
schedule_work(&(no_free_ptr(tt_work)->work));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void tt_del_tz_work_fn(struct work_struct *work)
|
|
{
|
|
struct tt_work *tt_work = tt_work_of_work(work);
|
|
struct tt_thermal_zone *tt_zone = tt_work->tt_zone;
|
|
|
|
kfree(tt_work);
|
|
|
|
debugfs_remove(tt_zone->d_tt_zone);
|
|
tt_zone_free(tt_zone);
|
|
}
|
|
|
|
static void tt_zone_unregister_tz(struct tt_thermal_zone *tt_zone)
|
|
{
|
|
guard(tt_zone)(tt_zone);
|
|
|
|
if (tt_zone->tz) {
|
|
thermal_zone_device_unregister(tt_zone->tz);
|
|
tt_zone->tz = NULL;
|
|
}
|
|
}
|
|
|
|
int tt_del_tz(const char *arg)
|
|
{
|
|
struct tt_work *tt_work __free(kfree);
|
|
struct tt_thermal_zone *tt_zone, *aux;
|
|
int ret;
|
|
int id;
|
|
|
|
ret = sscanf(arg, "%d", &id);
|
|
if (ret != 1)
|
|
return -EINVAL;
|
|
|
|
tt_work = kzalloc(sizeof(*tt_work), GFP_KERNEL);
|
|
if (!tt_work)
|
|
return -ENOMEM;
|
|
|
|
guard(mutex)(&tt_thermal_zones_lock);
|
|
|
|
ret = -EINVAL;
|
|
list_for_each_entry_safe(tt_zone, aux, &tt_thermal_zones, list_node) {
|
|
if (tt_zone->id == id) {
|
|
if (tt_zone->refcount) {
|
|
ret = -EBUSY;
|
|
} else {
|
|
list_del(&tt_zone->list_node);
|
|
ret = 0;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ret)
|
|
return ret;
|
|
|
|
tt_zone_unregister_tz(tt_zone);
|
|
|
|
INIT_WORK(&tt_work->work, tt_del_tz_work_fn);
|
|
tt_work->tt_zone = tt_zone;
|
|
schedule_work(&(no_free_ptr(tt_work)->work));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct tt_thermal_zone *tt_get_tt_zone(const char *arg)
|
|
{
|
|
struct tt_thermal_zone *tt_zone;
|
|
int ret, id;
|
|
|
|
ret = sscanf(arg, "%d", &id);
|
|
if (ret != 1)
|
|
return ERR_PTR(-EINVAL);
|
|
|
|
guard(mutex)(&tt_thermal_zones_lock);
|
|
|
|
ret = -EINVAL;
|
|
list_for_each_entry(tt_zone, &tt_thermal_zones, list_node) {
|
|
if (tt_zone->id == id) {
|
|
tt_zone->refcount++;
|
|
ret = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ret)
|
|
return ERR_PTR(ret);
|
|
|
|
return tt_zone;
|
|
}
|
|
|
|
static void tt_put_tt_zone(struct tt_thermal_zone *tt_zone)
|
|
{
|
|
guard(mutex)(&tt_thermal_zones_lock);
|
|
|
|
tt_zone->refcount--;
|
|
}
|
|
|
|
static void tt_zone_add_trip_work_fn(struct work_struct *work)
|
|
{
|
|
struct tt_work *tt_work = tt_work_of_work(work);
|
|
struct tt_thermal_zone *tt_zone = tt_work->tt_zone;
|
|
struct tt_trip *tt_trip = tt_work->tt_trip;
|
|
char d_name[TT_MAX_FILE_NAME_LENGTH];
|
|
|
|
kfree(tt_work);
|
|
|
|
snprintf(d_name, TT_MAX_FILE_NAME_LENGTH, "trip_%d_temp", tt_trip->id);
|
|
debugfs_create_file_unsafe(d_name, 0600, tt_zone->d_tt_zone,
|
|
&tt_trip->trip.temperature, &tt_int_attr);
|
|
|
|
snprintf(d_name, TT_MAX_FILE_NAME_LENGTH, "trip_%d_hyst", tt_trip->id);
|
|
debugfs_create_file_unsafe(d_name, 0600, tt_zone->d_tt_zone,
|
|
&tt_trip->trip.hysteresis, &tt_unsigned_int_attr);
|
|
|
|
tt_put_tt_zone(tt_zone);
|
|
}
|
|
|
|
int tt_zone_add_trip(const char *arg)
|
|
{
|
|
struct tt_work *tt_work __free(kfree);
|
|
struct tt_trip *tt_trip __free(kfree);
|
|
struct tt_thermal_zone *tt_zone;
|
|
int id;
|
|
|
|
tt_work = kzalloc(sizeof(*tt_work), GFP_KERNEL);
|
|
if (!tt_work)
|
|
return -ENOMEM;
|
|
|
|
tt_trip = kzalloc(sizeof(*tt_trip), GFP_KERNEL);
|
|
if (!tt_trip)
|
|
return -ENOMEM;
|
|
|
|
tt_zone = tt_get_tt_zone(arg);
|
|
if (IS_ERR(tt_zone))
|
|
return PTR_ERR(tt_zone);
|
|
|
|
id = ida_alloc(&tt_zone->ida, GFP_KERNEL);
|
|
if (id < 0) {
|
|
tt_put_tt_zone(tt_zone);
|
|
return id;
|
|
}
|
|
|
|
tt_trip->trip.type = THERMAL_TRIP_ACTIVE;
|
|
tt_trip->trip.temperature = THERMAL_TEMP_INVALID;
|
|
tt_trip->trip.flags = THERMAL_TRIP_FLAG_RW;
|
|
tt_trip->id = id;
|
|
|
|
guard(tt_zone)(tt_zone);
|
|
|
|
list_add_tail(&tt_trip->list_node, &tt_zone->trips);
|
|
tt_zone->num_trips++;
|
|
|
|
INIT_WORK(&tt_work->work, tt_zone_add_trip_work_fn);
|
|
tt_work->tt_zone = tt_zone;
|
|
tt_work->tt_trip = no_free_ptr(tt_trip);
|
|
schedule_work(&(no_free_ptr(tt_work)->work));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tt_zone_get_temp(struct thermal_zone_device *tz, int *temp)
|
|
{
|
|
struct tt_thermal_zone *tt_zone = thermal_zone_device_priv(tz);
|
|
|
|
*temp = READ_ONCE(tt_zone->tz_temp);
|
|
|
|
if (*temp < THERMAL_TEMP_INVALID)
|
|
return -ENODATA;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct thermal_zone_device_ops tt_zone_ops = {
|
|
.get_temp = tt_zone_get_temp,
|
|
};
|
|
|
|
static int tt_zone_register_tz(struct tt_thermal_zone *tt_zone)
|
|
{
|
|
struct thermal_trip *trips __free(kfree);
|
|
struct thermal_zone_device *tz;
|
|
struct tt_trip *tt_trip;
|
|
int i;
|
|
|
|
guard(tt_zone)(tt_zone);
|
|
|
|
if (tt_zone->tz)
|
|
return -EINVAL;
|
|
|
|
trips = kcalloc(tt_zone->num_trips, sizeof(*trips), GFP_KERNEL);
|
|
if (!trips)
|
|
return -ENOMEM;
|
|
|
|
i = 0;
|
|
list_for_each_entry(tt_trip, &tt_zone->trips, list_node)
|
|
trips[i++] = tt_trip->trip;
|
|
|
|
tt_zone->tz_temp = tt_zone->temp;
|
|
|
|
tz = thermal_zone_device_register_with_trips("test_tz", trips, i, tt_zone,
|
|
&tt_zone_ops, NULL, 0, 0);
|
|
if (IS_ERR(tz))
|
|
return PTR_ERR(tz);
|
|
|
|
tt_zone->tz = tz;
|
|
|
|
thermal_zone_device_enable(tz);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int tt_zone_reg(const char *arg)
|
|
{
|
|
struct tt_thermal_zone *tt_zone;
|
|
int ret;
|
|
|
|
tt_zone = tt_get_tt_zone(arg);
|
|
if (IS_ERR(tt_zone))
|
|
return PTR_ERR(tt_zone);
|
|
|
|
ret = tt_zone_register_tz(tt_zone);
|
|
|
|
tt_put_tt_zone(tt_zone);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int tt_zone_unreg(const char *arg)
|
|
{
|
|
struct tt_thermal_zone *tt_zone;
|
|
|
|
tt_zone = tt_get_tt_zone(arg);
|
|
if (IS_ERR(tt_zone))
|
|
return PTR_ERR(tt_zone);
|
|
|
|
tt_zone_unregister_tz(tt_zone);
|
|
|
|
tt_put_tt_zone(tt_zone);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void tt_zone_cleanup(void)
|
|
{
|
|
struct tt_thermal_zone *tt_zone, *aux;
|
|
|
|
list_for_each_entry_safe(tt_zone, aux, &tt_thermal_zones, list_node) {
|
|
tt_zone_unregister_tz(tt_zone);
|
|
|
|
list_del(&tt_zone->list_node);
|
|
|
|
tt_zone_free(tt_zone);
|
|
}
|
|
}
|