acpi_spmc: Add system power management controller driver

Add SPMC (system power management controller) driver as acpi_spmc. This
is the device which provides the LPI device D-state constraints and
allows for OSPM to send S0ix/modern standby entry/exit notifications.
This supports the original Intel DSM
(https://uefi.org/sites/default/files/resources/Intel_ACPI_Low_Power_S0_Idle.pdf,
untested), the AMD DSM (tested), and the Microsoft DSM (tested).

Before entry, acpi_spmc_check_constraints is called to notify of any
violated power constraints. This will use acpi_pwr_get_state to get
current device D-states when that gets added back.

Reviewed by:	olce
Tested by:	jkim, Oleksandr Kryvulia, Matthias Lanter
Approved by:	olce
Sponsored by:	The FreeBSD Foundation
Differential Revision:	https://reviews.freebsd.org/D48387
This commit is contained in:
Aymeric Wibo
2025-06-14 17:30:44 +02:00
parent a11d132f6c
commit c5daa5a4c3
4 changed files with 623 additions and 1 deletions
+3 -1
View File
@@ -486,10 +486,12 @@ Embedded controller driver
Fan driver
.It Li ACPI_OEM
Platform-specific driver for hotkeys, LED, etc.
.It Li ACPI_POWER
.It Li ACPI_POWERRES
Power resource driver
.It Li ACPI_PROCESSOR
CPU driver
.It Li ACPI_SPMC
System power management controller driver
.It Li ACPI_THERMAL
Thermal zone driver
.It Li ACPI_TIMER
+1
View File
@@ -776,6 +776,7 @@ dev/acpica/acpi_thermal.c optional acpi
dev/acpica/acpi_throttle.c optional acpi
dev/acpica/acpi_video.c optional acpi_video acpi
dev/acpica/acpi_dock.c optional acpi_dock acpi
dev/acpica/acpi_spmc.c optional acpi
dev/adlink/adlink.c optional adlink
dev/ae/if_ae.c optional ae pci
dev/age/if_age.c optional age pci
+1
View File
@@ -4618,6 +4618,7 @@ static struct debugtag dbg_layer[] = {
{"ACPI_FAN", ACPI_FAN},
{"ACPI_POWERRES", ACPI_POWERRES},
{"ACPI_PROCESSOR", ACPI_PROCESSOR},
{"ACPI_SPMC", ACPI_SPMC},
{"ACPI_THERMAL", ACPI_THERMAL},
{"ACPI_TIMER", ACPI_TIMER},
{"ACPI_ALL_DRIVERS", ACPI_ALL_DRIVERS},
+618
View File
@@ -0,0 +1,618 @@
/*
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2024-2025 The FreeBSD Foundation
*
* This software was developed by Aymeric Wibo <obiwac@freebsd.org>
* under sponsorship from the FreeBSD Foundation.
*/
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/bus.h>
#include <sys/malloc.h>
#include <sys/uuid.h>
#include <sys/kdb.h>
#include <contrib/dev/acpica/include/acpi.h>
#include <contrib/dev/acpica/include/accommon.h>
#include <dev/acpica/acpivar.h>
/* Hooks for the ACPI CA debugging infrastructure */
#define _COMPONENT ACPI_SPMC
ACPI_MODULE_NAME("SPMC")
static SYSCTL_NODE(_debug_acpi, OID_AUTO, spmc, CTLFLAG_RD | CTLFLAG_MPSAFE,
NULL, "SPMC debugging");
static char *spmc_ids[] = {
"PNP0D80",
NULL
};
enum intel_dsm_index {
DSM_ENUM_FUNCTIONS = 0,
DSM_GET_DEVICE_CONSTRAINTS = 1,
DSM_GET_CRASH_DUMP_DEVICE = 2,
DSM_DISPLAY_OFF_NOTIF = 3,
DSM_DISPLAY_ON_NOTIF = 4,
DSM_ENTRY_NOTIF = 5,
DSM_EXIT_NOTIF = 6,
/* Only for Microsoft DSM set. */
DSM_MODERN_ENTRY_NOTIF = 7,
DSM_MODERN_EXIT_NOTIF = 8,
};
enum amd_dsm_index {
AMD_DSM_ENUM_FUNCTIONS = 0,
AMD_DSM_GET_DEVICE_CONSTRAINTS = 1,
AMD_DSM_ENTRY_NOTIF = 2,
AMD_DSM_EXIT_NOTIF = 3,
AMD_DSM_DISPLAY_OFF_NOTIF = 4,
AMD_DSM_DISPLAY_ON_NOTIF = 5,
};
enum dsm_set_flags {
DSM_SET_INTEL = 1 << 0,
DSM_SET_MS = 1 << 1,
DSM_SET_AMD = 1 << 2,
};
struct dsm_set {
enum dsm_set_flags flag;
const char *name;
int revision;
struct uuid uuid;
uint64_t dsms_expected;
};
static struct dsm_set intel_dsm_set = {
.flag = DSM_SET_INTEL,
.name = "Intel",
/*
* XXX Linux uses 1 for the revision on Intel DSMs, but doesn't explain
* why. The commit that introduces this links to a document mentioning
* revision 0, so default this to 0.
*
* The debug.acpi.spmc.intel_dsm_revision sysctl may be used to configure
* this just in case.
*/
.revision = 0,
.uuid = { /* c4eb40a0-6cd2-11e2-bcfd-0800200c9a66 */
0xc4eb40a0, 0x6cd2, 0x11e2, 0xbc, 0xfd,
{0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66},
},
.dsms_expected = DSM_GET_DEVICE_CONSTRAINTS | DSM_DISPLAY_OFF_NOTIF |
DSM_DISPLAY_ON_NOTIF | DSM_ENTRY_NOTIF | DSM_EXIT_NOTIF,
};
SYSCTL_INT(_debug_acpi_spmc, OID_AUTO, intel_dsm_revision, CTLFLAG_RW,
&intel_dsm_set.revision, 0,
"Revision to use when evaluating Intel SPMC DSMs");
static struct dsm_set ms_dsm_set = {
.flag = DSM_SET_MS,
.name = "Microsoft",
.revision = 0,
.uuid = { /* 11e00d56-ce64-47ce-837b-1f898f9aa461 */
0x11e00d56, 0xce64, 0x47ce, 0x83, 0x7b,
{0x1f, 0x89, 0x8f, 0x9a, 0xa4, 0x61},
},
.dsms_expected = DSM_DISPLAY_OFF_NOTIF | DSM_DISPLAY_ON_NOTIF |
DSM_ENTRY_NOTIF | DSM_EXIT_NOTIF | DSM_MODERN_ENTRY_NOTIF |
DSM_MODERN_EXIT_NOTIF,
};
static struct dsm_set amd_dsm_set = {
.flag = DSM_SET_AMD,
.name = "AMD",
/*
* XXX Linux uses 0 for the revision on AMD DSMs, but at least on the
* Framework 13 AMD 7040 series, the enum functions DSM only returns a
* function mask that covers all the DSMs we need to call when called
* with revision 2.
*
* The debug.acpi.spmc.amd_dsm_revision sysctl may be used to configure
* this just in case.
*/
.revision = 2,
.uuid = { /* e3f32452-febc-43ce-9039-932122d37721 */
0xe3f32452, 0xfebc, 0x43ce, 0x90, 0x39,
{0x93, 0x21, 0x22, 0xd3, 0x77, 0x21},
},
.dsms_expected = AMD_DSM_GET_DEVICE_CONSTRAINTS | AMD_DSM_ENTRY_NOTIF |
AMD_DSM_EXIT_NOTIF | AMD_DSM_DISPLAY_OFF_NOTIF |
AMD_DSM_DISPLAY_ON_NOTIF,
};
SYSCTL_INT(_debug_acpi_spmc, OID_AUTO, amd_dsm_revision, CTLFLAG_RW,
&amd_dsm_set.revision, 0, "Revision to use when evaluating AMD SPMC DSMs");
union dsm_index {
int i;
enum intel_dsm_index regular;
enum amd_dsm_index amd;
};
struct acpi_spmc_constraint {
bool enabled;
char *name;
int min_d_state;
ACPI_HANDLE handle;
/* Unused, spec only. */
uint64_t lpi_uid;
uint64_t min_dev_specific_state;
/* Unused, AMD only. */
uint64_t function_states;
};
struct acpi_spmc_softc {
device_t dev;
ACPI_HANDLE handle;
ACPI_OBJECT *obj;
enum dsm_set_flags dsm_sets;
bool constraints_populated;
size_t constraint_count;
struct acpi_spmc_constraint *constraints;
};
static void acpi_spmc_check_dsm_set(struct acpi_spmc_softc *sc,
ACPI_HANDLE handle, struct dsm_set *dsm_set);
static int acpi_spmc_get_constraints(device_t dev);
static void acpi_spmc_free_constraints(struct acpi_spmc_softc *sc);
static int
acpi_spmc_probe(device_t dev)
{
char *name;
ACPI_HANDLE handle;
struct acpi_spmc_softc *sc;
/* Check that this is an enabled device. */
if (acpi_get_type(dev) != ACPI_TYPE_DEVICE || acpi_disabled("spmc"))
return (ENXIO);
if (ACPI_ID_PROBE(device_get_parent(dev), dev, spmc_ids, &name) > 0)
return (ENXIO);
handle = acpi_get_handle(dev);
if (handle == NULL)
return (ENXIO);
sc = device_get_softc(dev);
/* Check which sets of DSM's are supported. */
sc->dsm_sets = 0;
acpi_spmc_check_dsm_set(sc, handle, &intel_dsm_set);
acpi_spmc_check_dsm_set(sc, handle, &ms_dsm_set);
acpi_spmc_check_dsm_set(sc, handle, &amd_dsm_set);
if (sc->dsm_sets == 0)
return (ENXIO);
device_set_descf(dev, "Low Power S0 Idle (DSM sets 0x%x)",
sc->dsm_sets);
return (0);
}
static int
acpi_spmc_attach(device_t dev)
{
struct acpi_spmc_softc *sc;
sc = device_get_softc(dev);
sc->dev = dev;
sc->handle = acpi_get_handle(dev);
if (sc->handle == NULL)
return (ENXIO);
sc->constraints_populated = false;
sc->constraint_count = 0;
sc->constraints = NULL;
/* Get device constraints. We can only call this once so do this now. */
acpi_spmc_get_constraints(sc->dev);
return (0);
}
static int
acpi_spmc_detach(device_t dev)
{
acpi_spmc_free_constraints(device_get_softc(dev));
return (0);
}
static void
acpi_spmc_check_dsm_set(struct acpi_spmc_softc *sc, ACPI_HANDLE handle,
struct dsm_set *dsm_set)
{
const uint64_t dsms_supported = acpi_DSMQuery(handle,
(uint8_t *)&dsm_set->uuid, dsm_set->revision);
/*
* Check if DSM set supported at all. We do this by checking the
* existence of "enum functions".
*/
if ((dsms_supported & 1) == 0)
return;
if ((dsms_supported & dsm_set->dsms_expected)
!= dsm_set->dsms_expected) {
device_printf(sc->dev, "DSM set %s does not support expected "
"DSMs (0x%lx vs 0x%lx). Some methods may fail.\n",
dsm_set->name, dsms_supported, dsm_set->dsms_expected);
}
sc->dsm_sets |= dsm_set->flag;
}
static void
acpi_spmc_free_constraints(struct acpi_spmc_softc *sc)
{
if (sc->constraints == NULL)
return;
for (size_t i = 0; i < sc->constraint_count; i++) {
if (sc->constraints[i].name != NULL)
free(sc->constraints[i].name, M_TEMP);
}
free(sc->constraints, M_TEMP);
sc->constraints = NULL;
}
static int
acpi_spmc_get_constraints_spec(struct acpi_spmc_softc *sc, ACPI_OBJECT *object)
{
struct acpi_spmc_constraint *constraint;
int revision;
ACPI_OBJECT *constraint_obj;
ACPI_OBJECT *name_obj;
ACPI_OBJECT *detail;
ACPI_OBJECT *constraint_package;
KASSERT(sc->constraints_populated == false,
("constraints already populated"));
sc->constraint_count = object->Package.Count;
sc->constraints = malloc(sc->constraint_count * sizeof *sc->constraints,
M_TEMP, M_WAITOK | M_ZERO);
/*
* The value of sc->constraint_count can change during the loop, so
* iterate until object->Package.Count so we actually go over all
* elements in the package.
*/
for (size_t i = 0; i < object->Package.Count; i++) {
constraint_obj = &object->Package.Elements[i];
constraint = &sc->constraints[i];
constraint->enabled =
constraint_obj->Package.Elements[1].Integer.Value;
name_obj = &constraint_obj->Package.Elements[0];
constraint->name = strdup(name_obj->String.Pointer, M_TEMP);
if (constraint->name == NULL) {
acpi_spmc_free_constraints(sc);
return (ENOMEM);
}
/*
* The first element in the device constraint detail package is
* the revision, and should always be zero.
*/
revision = constraint_obj->Package.Elements[0].Integer.Value;
if (revision != 0) {
device_printf(sc->dev, "Unknown revision %d for "
"device constraint detail package\n", revision);
sc->constraint_count--;
continue;
}
detail = &constraint_obj->Package.Elements[2];
constraint_package = &detail->Package.Elements[1];
constraint->lpi_uid =
constraint_package->Package.Elements[0].Integer.Value;
constraint->min_d_state =
constraint_package->Package.Elements[1].Integer.Value;
constraint->min_dev_specific_state =
constraint_package->Package.Elements[2].Integer.Value;
}
sc->constraints_populated = true;
return (0);
}
static int
acpi_spmc_get_constraints_amd(struct acpi_spmc_softc *sc, ACPI_OBJECT *object)
{
size_t constraint_count;
ACPI_OBJECT *constraint_obj;
ACPI_OBJECT *constraints;
struct acpi_spmc_constraint *constraint;
ACPI_OBJECT *name_obj;
KASSERT(sc->constraints_populated == false,
("constraints already populated"));
/*
* First element in the package is unknown.
* Second element is the number of device constraints.
* Third element is the list of device constraints itself.
*/
constraint_count = object->Package.Elements[1].Integer.Value;
constraints = &object->Package.Elements[2];
if (constraints->Package.Count != constraint_count) {
device_printf(sc->dev, "constraint count mismatch (%d to %zu)\n",
constraints->Package.Count, constraint_count);
return (ENXIO);
}
sc->constraint_count = constraint_count;
sc->constraints = malloc(constraint_count * sizeof *sc->constraints,
M_TEMP, M_WAITOK | M_ZERO);
for (size_t i = 0; i < constraint_count; i++) {
/* Parse the constraint package. */
constraint_obj = &constraints->Package.Elements[i];
if (constraint_obj->Package.Count != 4) {
device_printf(sc->dev, "constraint %zu has %d elements\n",
i, constraint_obj->Package.Count);
acpi_spmc_free_constraints(sc);
return (ENXIO);
}
constraint = &sc->constraints[i];
constraint->enabled =
constraint_obj->Package.Elements[0].Integer.Value;
name_obj = &constraint_obj->Package.Elements[1];
constraint->name = strdup(name_obj->String.Pointer, M_TEMP);
if (constraint->name == NULL) {
acpi_spmc_free_constraints(sc);
return (ENOMEM);
}
constraint->function_states =
constraint_obj->Package.Elements[2].Integer.Value;
constraint->min_d_state =
constraint_obj->Package.Elements[3].Integer.Value;
}
sc->constraints_populated = true;
return (0);
}
static int
acpi_spmc_get_constraints(device_t dev)
{
struct acpi_spmc_softc *sc;
union dsm_index dsm_index;
struct dsm_set *dsm_set;
ACPI_STATUS status;
ACPI_BUFFER result;
ACPI_OBJECT *object;
bool is_amd;
int rv;
struct acpi_spmc_constraint *constraint;
sc = device_get_softc(dev);
if (sc->constraints_populated)
return (0);
/* The Microsoft DSM set doesn't have this DSM. */
is_amd = (sc->dsm_sets & DSM_SET_AMD) != 0;
if (is_amd) {
dsm_set = &amd_dsm_set;
dsm_index.amd = AMD_DSM_GET_DEVICE_CONSTRAINTS;
} else {
dsm_set = &intel_dsm_set;
dsm_index.regular = DSM_GET_DEVICE_CONSTRAINTS;
}
/* XXX It seems like this DSM fails if called more than once. */
status = acpi_EvaluateDSMTyped(sc->handle, (uint8_t *)&dsm_set->uuid,
dsm_set->revision, dsm_index.i, NULL, &result,
ACPI_TYPE_PACKAGE);
if (ACPI_FAILURE(status)) {
device_printf(dev, "%s failed to call %s DSM %d (rev %d)\n",
__func__, dsm_set->name, dsm_index.i, dsm_set->revision);
return (ENXIO);
}
object = (ACPI_OBJECT *)result.Pointer;
if (is_amd)
rv = acpi_spmc_get_constraints_amd(sc, object);
else
rv = acpi_spmc_get_constraints_spec(sc, object);
AcpiOsFree(object);
if (rv != 0)
return (rv);
/* Get handles for each constraint device. */
for (size_t i = 0; i < sc->constraint_count; i++) {
constraint = &sc->constraints[i];
status = acpi_GetHandleInScope(sc->handle,
__DECONST(char *, constraint->name), &constraint->handle);
if (ACPI_FAILURE(status)) {
device_printf(dev, "failed to get handle for %s\n",
constraint->name);
constraint->handle = NULL;
}
}
return (0);
}
static void
acpi_spmc_check_constraints(struct acpi_spmc_softc *sc)
{
bool violation = false;
KASSERT(sc->constraints_populated, ("constraints not populated"));
for (size_t i = 0; i < sc->constraint_count; i++) {
struct acpi_spmc_constraint *constraint = &sc->constraints[i];
if (!constraint->enabled)
continue;
if (constraint->handle == NULL)
continue;
ACPI_STATUS status = acpi_GetHandleInScope(sc->handle,
__DECONST(char *, constraint->name), &constraint->handle);
if (ACPI_FAILURE(status)) {
device_printf(sc->dev, "failed to get handle for %s\n",
constraint->name);
constraint->handle = NULL;
}
if (constraint->handle == NULL)
continue;
#ifdef notyet
int d_state;
if (ACPI_FAILURE(acpi_pwr_get_state(constraint->handle, &d_state)))
continue;
if (d_state < constraint->min_d_state) {
device_printf(sc->dev, "constraint for device %s"
" violated (minimum D-state required was %s, actual"
" D-state is %s), might fail to enter LPI state\n",
constraint->name,
acpi_d_state_to_str(constraint->min_d_state),
acpi_d_state_to_str(d_state));
violation = true;
}
#endif
}
if (!violation)
device_printf(sc->dev,
"all device power constraints respected!\n");
}
static void
acpi_spmc_run_dsm(device_t dev, struct dsm_set *dsm_set, int index)
{
struct acpi_spmc_softc *sc;
ACPI_STATUS status;
ACPI_BUFFER result;
sc = device_get_softc(dev);
status = acpi_EvaluateDSMTyped(sc->handle, (uint8_t *)&dsm_set->uuid,
dsm_set->revision, index, NULL, &result, ACPI_TYPE_ANY);
if (ACPI_FAILURE(status)) {
device_printf(dev, "%s failed to call %s DSM %d (rev %d)\n",
__func__, dsm_set->name, index, dsm_set->revision);
return;
}
AcpiOsFree(result.Pointer);
}
/*
* Try running the DSMs from all the DSM sets we have, as them failing costs us
* nothing, and it seems like on AMD platforms, both the AMD entry and Microsoft
* "modern" DSM's are required for it to enter modern standby.
*
* This is what Linux does too.
*/
static void
acpi_spmc_display_off_notif(device_t dev)
{
struct acpi_spmc_softc *sc = device_get_softc(dev);
if ((sc->dsm_sets & DSM_SET_INTEL) != 0)
acpi_spmc_run_dsm(dev, &intel_dsm_set, DSM_DISPLAY_OFF_NOTIF);
if ((sc->dsm_sets & DSM_SET_MS) != 0)
acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_DISPLAY_OFF_NOTIF);
if ((sc->dsm_sets & DSM_SET_AMD) != 0)
acpi_spmc_run_dsm(dev, &amd_dsm_set, AMD_DSM_DISPLAY_OFF_NOTIF);
}
static void
acpi_spmc_display_on_notif(device_t dev)
{
struct acpi_spmc_softc *sc = device_get_softc(dev);
if ((sc->dsm_sets & DSM_SET_INTEL) != 0)
acpi_spmc_run_dsm(dev, &intel_dsm_set, DSM_DISPLAY_ON_NOTIF);
if ((sc->dsm_sets & DSM_SET_MS) != 0)
acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_DISPLAY_ON_NOTIF);
if ((sc->dsm_sets & DSM_SET_AMD) != 0)
acpi_spmc_run_dsm(dev, &amd_dsm_set, AMD_DSM_DISPLAY_ON_NOTIF);
}
static void
acpi_spmc_entry_notif(device_t dev)
{
struct acpi_spmc_softc *sc = device_get_softc(dev);
acpi_spmc_check_constraints(sc);
if ((sc->dsm_sets & DSM_SET_AMD) != 0)
acpi_spmc_run_dsm(dev, &amd_dsm_set, AMD_DSM_ENTRY_NOTIF);
if ((sc->dsm_sets & DSM_SET_MS) != 0) {
acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_MODERN_ENTRY_NOTIF);
acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_ENTRY_NOTIF);
}
if ((sc->dsm_sets & DSM_SET_INTEL) != 0)
acpi_spmc_run_dsm(dev, &intel_dsm_set, DSM_ENTRY_NOTIF);
}
static void
acpi_spmc_exit_notif(device_t dev)
{
struct acpi_spmc_softc *sc = device_get_softc(dev);
if ((sc->dsm_sets & DSM_SET_INTEL) != 0)
acpi_spmc_run_dsm(dev, &intel_dsm_set, DSM_EXIT_NOTIF);
if ((sc->dsm_sets & DSM_SET_AMD) != 0)
acpi_spmc_run_dsm(dev, &amd_dsm_set, AMD_DSM_EXIT_NOTIF);
if ((sc->dsm_sets & DSM_SET_MS) != 0) {
acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_EXIT_NOTIF);
acpi_spmc_run_dsm(dev, &ms_dsm_set, DSM_MODERN_EXIT_NOTIF);
}
}
static int
acpi_spmc_suspend(device_t dev)
{
acpi_spmc_display_off_notif(dev);
acpi_spmc_entry_notif(dev);
return (0);
}
static int
acpi_spmc_resume(device_t dev)
{
acpi_spmc_exit_notif(dev);
acpi_spmc_display_on_notif(dev);
return (0);
}
static device_method_t acpi_spmc_methods[] = {
DEVMETHOD(device_probe, acpi_spmc_probe),
DEVMETHOD(device_attach, acpi_spmc_attach),
DEVMETHOD(device_detach, acpi_spmc_detach),
DEVMETHOD_END
};
static driver_t acpi_spmc_driver = {
"acpi_spmc",
acpi_spmc_methods,
sizeof(struct acpi_spmc_softc),
};
DRIVER_MODULE_ORDERED(acpi_spmc, acpi, acpi_spmc_driver, NULL, NULL, SI_ORDER_ANY);
MODULE_DEPEND(acpi_spmc, acpi, 1, 1, 1);