ACPI: fan: Expose fan performance state information

When _FPS indicates variable speed fan support, the thermal cooling
device for fan shows max performance state count as "max_state"
(greater than or equal to 1).

But the thermal cooling device doesn't expose the properties of each
performance state. This is not enough for smart fan control user
space software, which also considers speed, power and noise level.

This change exposes the properties of the fan performance states
in the sysfs directory of the ACPI device representing the fan,
that is

/sys/bus/acpi/devices/devices/INT3404:00

or

/sys/bus/platform/devices/PNP0C0B:00.

For example:

$ ls /sys/bus/acpi/devices/INT3404\:00
description  path           state0   state11  state4  state7  status
hid          physical_node  state1   state2   state5  state8  subsystem
modalias     power          state10  state3   state6  state9  uevent
uid          wakeup

where each state* attribute lists the properties of a fan performance
state in the following format:

control_percent:trip_point:speed_rpm:noise_level_mdb:power_mw

$ cat /sys/bus/acpi/devices/INT3404\:00/state10
95:0:11600:47500:4500

as documented in

Documentation/admin-guide/acpi/fan_performance_states.rst

While at it, return the correct error code from acpi_fan_probe()
when acpi_fan_get_fps() or acpi_fan_get_fif() fails.

Suggested-by: Rafael J. Wysocki <rafael.j.wysocki@intel.com>
Signed-off-by: Srinivas Pandruvada <srinivas.pandruvada@linux.intel.com>
[ rjw: Subject, changelog, documentation ]
Signed-off-by: Rafael J. Wysocki <rafael.j.wysocki@intel.com>
This commit is contained in:
Srinivas Pandruvada 2019-12-13 15:48:40 -08:00 committed by Rafael J. Wysocki
parent d1eef1c619
commit d19e470b66
3 changed files with 151 additions and 8 deletions

View File

@ -0,0 +1,62 @@
.. SPDX-License-Identifier: GPL-2.0
===========================
ACPI Fan Performance States
===========================
When the optional _FPS object is present under an ACPI device representing a
fan (for example, PNP0C0B or INT3404), the ACPI fan driver creates additional
"state*" attributes in the sysfs directory of the ACPI device in question.
These attributes list properties of fan performance states.
For more information on _FPS refer to the ACPI specification at:
http://uefi.org/specifications
For instance, the contents of the INT3404 ACPI device sysfs directory
may look as follows::
$ ls -l /sys/bus/acpi/devices/INT3404:00/
total 0
...
-r--r--r-- 1 root root 4096 Dec 13 20:38 state0
-r--r--r-- 1 root root 4096 Dec 13 20:38 state1
-r--r--r-- 1 root root 4096 Dec 13 20:38 state10
-r--r--r-- 1 root root 4096 Dec 13 20:38 state11
-r--r--r-- 1 root root 4096 Dec 13 20:38 state2
-r--r--r-- 1 root root 4096 Dec 13 20:38 state3
-r--r--r-- 1 root root 4096 Dec 13 20:38 state4
-r--r--r-- 1 root root 4096 Dec 13 20:38 state5
-r--r--r-- 1 root root 4096 Dec 13 20:38 state6
-r--r--r-- 1 root root 4096 Dec 13 20:38 state7
-r--r--r-- 1 root root 4096 Dec 13 20:38 state8
-r--r--r-- 1 root root 4096 Dec 13 20:38 state9
-r--r--r-- 1 root root 4096 Dec 13 01:00 status
...
where each of the "state*" files represents one performance state of the fan
and contains a colon-separated list of 5 integer numbers (fields) with the
following interpretation::
control_percent:trip_point_index:speed_rpm:noise_level_mdb:power_mw
* ``control_percent``: The percent value to be used to set the fan speed to a
specific level using the _FSL object (0-100).
* ``trip_point_index``: The active cooling trip point number that corresponds
to this performance state (0-9).
* ``speed_rpm``: Speed of the fan in rotations per minute.
* ``noise_level_mdb``: Audible noise emitted by the fan in this state in
millidecibels.
* ``power_mw``: Power draw of the fan in this state in milliwatts.
For example::
$cat /sys/bus/acpi/devices/INT3404:00/state1
25:0:3200:12500:1250
When a given field is not populated or its value provided by the platform
firmware is invalid, the "not-defined" string is shown instead of the value.

View File

@ -12,3 +12,4 @@ the Linux ACPI support.
dsdt-override dsdt-override
ssdt-overlays ssdt-overlays
cppc_sysfs cppc_sysfs
fan_performance_states

View File

@ -44,12 +44,16 @@ static const struct dev_pm_ops acpi_fan_pm = {
#define FAN_PM_OPS_PTR NULL #define FAN_PM_OPS_PTR NULL
#endif #endif
#define ACPI_FPS_NAME_LEN 20
struct acpi_fan_fps { struct acpi_fan_fps {
u64 control; u64 control;
u64 trip_point; u64 trip_point;
u64 speed; u64 speed;
u64 noise_level; u64 noise_level;
u64 power; u64 power;
char name[ACPI_FPS_NAME_LEN];
struct device_attribute dev_attr;
}; };
struct acpi_fan_fif { struct acpi_fan_fif {
@ -265,6 +269,39 @@ static int acpi_fan_speed_cmp(const void *a, const void *b)
return fps1->speed - fps2->speed; return fps1->speed - fps2->speed;
} }
static ssize_t show_state(struct device *dev, struct device_attribute *attr, char *buf)
{
struct acpi_fan_fps *fps = container_of(attr, struct acpi_fan_fps, dev_attr);
int count;
if (fps->control == 0xFFFFFFFF || fps->control > 100)
count = snprintf(buf, PAGE_SIZE, "not-defined:");
else
count = snprintf(buf, PAGE_SIZE, "%lld:", fps->control);
if (fps->trip_point == 0xFFFFFFFF || fps->trip_point > 9)
count += snprintf(&buf[count], PAGE_SIZE, "not-defined:");
else
count += snprintf(&buf[count], PAGE_SIZE, "%lld:", fps->trip_point);
if (fps->speed == 0xFFFFFFFF)
count += snprintf(&buf[count], PAGE_SIZE, "not-defined:");
else
count += snprintf(&buf[count], PAGE_SIZE, "%lld:", fps->speed);
if (fps->noise_level == 0xFFFFFFFF)
count += snprintf(&buf[count], PAGE_SIZE, "not-defined:");
else
count += snprintf(&buf[count], PAGE_SIZE, "%lld:", fps->noise_level * 100);
if (fps->power == 0xFFFFFFFF)
count += snprintf(&buf[count], PAGE_SIZE, "not-defined\n");
else
count += snprintf(&buf[count], PAGE_SIZE, "%lld\n", fps->power);
return count;
}
static int acpi_fan_get_fps(struct acpi_device *device) static int acpi_fan_get_fps(struct acpi_device *device)
{ {
struct acpi_fan *fan = acpi_driver_data(device); struct acpi_fan *fan = acpi_driver_data(device);
@ -295,12 +332,13 @@ static int acpi_fan_get_fps(struct acpi_device *device)
} }
for (i = 0; i < fan->fps_count; i++) { for (i = 0; i < fan->fps_count; i++) {
struct acpi_buffer format = { sizeof("NNNNN"), "NNNNN" }; struct acpi_buffer format = { sizeof("NNNNN"), "NNNNN" };
struct acpi_buffer fps = { sizeof(fan->fps[i]), &fan->fps[i] }; struct acpi_buffer fps = { offsetof(struct acpi_fan_fps, name),
&fan->fps[i] };
status = acpi_extract_package(&obj->package.elements[i + 1], status = acpi_extract_package(&obj->package.elements[i + 1],
&format, &fps); &format, &fps);
if (ACPI_FAILURE(status)) { if (ACPI_FAILURE(status)) {
dev_err(&device->dev, "Invalid _FPS element\n"); dev_err(&device->dev, "Invalid _FPS element\n");
break; goto err;
} }
} }
@ -308,6 +346,24 @@ static int acpi_fan_get_fps(struct acpi_device *device)
sort(fan->fps, fan->fps_count, sizeof(*fan->fps), sort(fan->fps, fan->fps_count, sizeof(*fan->fps),
acpi_fan_speed_cmp, NULL); acpi_fan_speed_cmp, NULL);
for (i = 0; i < fan->fps_count; ++i) {
struct acpi_fan_fps *fps = &fan->fps[i];
snprintf(fps->name, ACPI_FPS_NAME_LEN, "state%d", i);
fps->dev_attr.show = show_state;
fps->dev_attr.store = NULL;
fps->dev_attr.attr.name = fps->name;
fps->dev_attr.attr.mode = 0444;
status = sysfs_create_file(&device->dev.kobj, &fps->dev_attr.attr);
if (status) {
int j;
for (j = 0; j < i; ++j)
sysfs_remove_file(&device->dev.kobj, &fan->fps[j].dev_attr.attr);
break;
}
}
err: err:
kfree(obj); kfree(obj);
return status; return status;
@ -330,14 +386,20 @@ static int acpi_fan_probe(struct platform_device *pdev)
platform_set_drvdata(pdev, fan); platform_set_drvdata(pdev, fan);
if (acpi_fan_is_acpi4(device)) { if (acpi_fan_is_acpi4(device)) {
if (acpi_fan_get_fif(device) || acpi_fan_get_fps(device)) result = acpi_fan_get_fif(device);
goto end; if (result)
return result;
result = acpi_fan_get_fps(device);
if (result)
return result;
fan->acpi4 = true; fan->acpi4 = true;
} else { } else {
result = acpi_device_update_power(device, NULL); result = acpi_device_update_power(device, NULL);
if (result) { if (result) {
dev_err(&device->dev, "Failed to set initial power state\n"); dev_err(&device->dev, "Failed to set initial power state\n");
goto end; goto err_end;
} }
} }
@ -350,7 +412,7 @@ static int acpi_fan_probe(struct platform_device *pdev)
&fan_cooling_ops); &fan_cooling_ops);
if (IS_ERR(cdev)) { if (IS_ERR(cdev)) {
result = PTR_ERR(cdev); result = PTR_ERR(cdev);
goto end; goto err_end;
} }
dev_dbg(&pdev->dev, "registered as cooling_device%d\n", cdev->id); dev_dbg(&pdev->dev, "registered as cooling_device%d\n", cdev->id);
@ -365,10 +427,21 @@ static int acpi_fan_probe(struct platform_device *pdev)
result = sysfs_create_link(&cdev->device.kobj, result = sysfs_create_link(&cdev->device.kobj,
&pdev->dev.kobj, &pdev->dev.kobj,
"device"); "device");
if (result) if (result) {
dev_err(&pdev->dev, "Failed to create sysfs link 'device'\n"); dev_err(&pdev->dev, "Failed to create sysfs link 'device'\n");
goto err_end;
}
return 0;
err_end:
if (fan->acpi4) {
int i;
for (i = 0; i < fan->fps_count; ++i)
sysfs_remove_file(&device->dev.kobj, &fan->fps[i].dev_attr.attr);
}
end:
return result; return result;
} }
@ -376,6 +449,13 @@ static int acpi_fan_remove(struct platform_device *pdev)
{ {
struct acpi_fan *fan = platform_get_drvdata(pdev); struct acpi_fan *fan = platform_get_drvdata(pdev);
if (fan->acpi4) {
struct acpi_device *device = ACPI_COMPANION(&pdev->dev);
int i;
for (i = 0; i < fan->fps_count; ++i)
sysfs_remove_file(&device->dev.kobj, &fan->fps[i].dev_attr.attr);
}
sysfs_remove_link(&pdev->dev.kobj, "thermal_cooling"); sysfs_remove_link(&pdev->dev.kobj, "thermal_cooling");
sysfs_remove_link(&fan->cdev->device.kobj, "device"); sysfs_remove_link(&fan->cdev->device.kobj, "device");
thermal_cooling_device_unregister(fan->cdev); thermal_cooling_device_unregister(fan->cdev);