The AC adapter device communicates AC power state to the guest via ACPI. AC adapter state is controlled programmatically via QMP commands, ensuring deterministic behavior.
Properties: - 'ioport': I/O port base address (default: 0x53c) The device implements the ACPI_DEV_AML_IF interface to generate its own AML code, placing the ADP0 device directly under \_SB scope. QMP commands: - ac-adapter-set-state: Set AC adapter connection state - query-ac-adapter: Query current AC adapter state Signed-off-by: Leonid Bloch <[email protected]> --- MAINTAINERS | 2 + hw/acpi/Kconfig | 4 + hw/acpi/acad-stub.c | 20 +++ hw/acpi/acad.c | 251 +++++++++++++++++++++++++++ hw/acpi/meson.build | 2 + hw/acpi/trace-events | 4 + hw/i386/Kconfig | 1 + include/hw/acpi/acad.h | 25 +++ include/hw/acpi/acpi_dev_interface.h | 1 + qapi/acpi.json | 47 +++++ 10 files changed, 357 insertions(+) create mode 100644 hw/acpi/acad-stub.c create mode 100644 hw/acpi/acad.c create mode 100644 include/hw/acpi/acad.h diff --git a/MAINTAINERS b/MAINTAINERS index 47436ec878..90941519e3 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -3047,6 +3047,8 @@ AC Adapter M: Leonid Bloch <[email protected]> S: Maintained F: docs/specs/acad.rst +F: hw/acpi/acad* +F: include/hw/acpi/acad.h Subsystems ---------- diff --git a/hw/acpi/Kconfig b/hw/acpi/Kconfig index 6b2c46d37a..889ace2dfa 100644 --- a/hw/acpi/Kconfig +++ b/hw/acpi/Kconfig @@ -74,6 +74,10 @@ config ACPI_VIOT bool depends on ACPI +config AC_ADAPTER + bool + depends on ACPI + config ACPI_HW_REDUCED bool select ACPI diff --git a/hw/acpi/acad-stub.c b/hw/acpi/acad-stub.c new file mode 100644 index 0000000000..92093b7682 --- /dev/null +++ b/hw/acpi/acad-stub.c @@ -0,0 +1,20 @@ +/* + * QEMU emulated AC adapter device - QMP stubs. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "qapi/qapi-commands-acpi.h" + +void qmp_ac_adapter_set_state(bool connected, Error **errp) +{ + error_setg(errp, "No AC adapter device found"); +} + +AcAdapterInfo *qmp_query_ac_adapter(Error **errp) +{ + error_setg(errp, "No AC adapter device found"); + return NULL; +} diff --git a/hw/acpi/acad.c b/hw/acpi/acad.c new file mode 100644 index 0000000000..11777fbb3c --- /dev/null +++ b/hw/acpi/acad.c @@ -0,0 +1,251 @@ +/* + * QEMU emulated AC adapter device. + * + * Copyright (c) 2019-2026 Janus Technologies, Inc. (http://janustech.com) + * + * Authors: + * Leonid Bloch <[email protected]> + * Marcel Apfelbaum <[email protected]> + * Dmitry Fleytman <[email protected]> + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + */ + +#include "qemu/osdep.h" +#include "trace.h" +#include "hw/isa/isa.h" +#include "hw/acpi/acpi.h" +#include "qapi/error.h" +#include "hw/core/qdev-properties.h" +#include "migration/vmstate.h" +#include "hw/acpi/acpi_aml_interface.h" +#include "qapi/qapi-commands-acpi.h" + +#include "hw/acpi/acad.h" + +#define AC_ADAPTER_DEVICE(obj) OBJECT_CHECK(ACADState, (obj), \ + TYPE_AC_ADAPTER) + +#define AC_STA_ADDR 0 + +enum { + AC_ADAPTER_OFFLINE = 0, + AC_ADAPTER_ONLINE = 1, +}; + +typedef struct ACADState { + ISADevice dev; + MemoryRegion io; + uint16_t ioport; + uint8_t state; + bool qmp_connected; +} ACADState; + +static void acad_get_dynamic_status(ACADState *s) +{ + s->state = s->qmp_connected ? AC_ADAPTER_ONLINE : AC_ADAPTER_OFFLINE; + + trace_acad_get_dynamic_status(s->state); +} + +static void acad_realize(DeviceState *dev, Error **errp) +{ + ISADevice *d = ISA_DEVICE(dev); + ACADState *s = AC_ADAPTER_DEVICE(dev); + bool ambiguous; + + trace_acad_realize(); + + object_resolve_path_type("", TYPE_AC_ADAPTER, &ambiguous); + if (ambiguous) { + error_setg(errp, "at most one %s device is permitted", + TYPE_AC_ADAPTER); + return; + } + + /* Initialize to disconnected by default */ + s->qmp_connected = false; + + isa_register_ioport(d, &s->io, s->ioport); +} + +static const Property acad_device_properties[] = { + DEFINE_PROP_UINT16(AC_ADAPTER_IOPORT_PROP, ACADState, ioport, 0x53c), +}; + +static const VMStateDescription acad_vmstate = { + .name = "acad", + .version_id = 1, + .minimum_version_id = 1, + .fields = (VMStateField[]) { + VMSTATE_BOOL(qmp_connected, ACADState), + VMSTATE_END_OF_LIST() + } +}; + +static void build_acad_aml(AcpiDevAmlIf *adev, Aml *scope) +{ + Aml *dev, *field, *method, *pkg; + Aml *acad_state; + Aml *sb_scope; + ACADState *s = AC_ADAPTER_DEVICE(adev); + + acad_state = aml_local(0); + + sb_scope = aml_scope("\\_SB"); + dev = aml_device("ADP0"); + aml_append(dev, aml_name_decl("_HID", aml_string("ACPI0003"))); + + aml_append(dev, aml_operation_region("ACST", AML_SYSTEM_IO, + aml_int(s->ioport), + AC_ADAPTER_LEN)); + field = aml_field("ACST", AML_BYTE_ACC, AML_NOLOCK, AML_PRESERVE); + aml_append(field, aml_named_field("PWRS", 8)); + aml_append(dev, field); + + method = aml_method("_PSR", 0, AML_NOTSERIALIZED); + aml_append(method, aml_store(aml_name("PWRS"), acad_state)); + aml_append(method, aml_return(acad_state)); + aml_append(dev, method); + + method = aml_method("_PCL", 0, AML_NOTSERIALIZED); + pkg = aml_package(1); + aml_append(pkg, aml_name("_SB")); + aml_append(method, aml_return(pkg)); + aml_append(dev, method); + + method = aml_method("_PIF", 0, AML_NOTSERIALIZED); + pkg = aml_package(6); + /* Power Source State */ + aml_append(pkg, aml_int(0)); /* Non-redundant, non-shared */ + /* Maximum Output Power */ + aml_append(pkg, aml_int(AC_ADAPTER_VAL_UNKNOWN)); + /* Maximum Input Power */ + aml_append(pkg, aml_int(AC_ADAPTER_VAL_UNKNOWN)); + /* Model Number */ + aml_append(pkg, aml_string("QADP001")); + /* Serial Number */ + aml_append(pkg, aml_string("SN00000")); + /* OEM Information */ + aml_append(pkg, aml_string("QEMU")); + aml_append(method, aml_return(pkg)); + aml_append(dev, method); + + aml_append(sb_scope, dev); + aml_append(scope, sb_scope); + + /* Status Change */ + method = aml_method("\\_GPE._E0B", 0, AML_NOTSERIALIZED); + aml_append(method, aml_notify(aml_name("\\_SB.ADP0"), aml_int(0x80))); + aml_append(scope, method); +} + +static void acad_class_init(ObjectClass *class, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(class); + AcpiDevAmlIfClass *adevc = ACPI_DEV_AML_IF_CLASS(class); + + dc->realize = acad_realize; + dc->hotpluggable = false; + device_class_set_props(dc, acad_device_properties); + dc->vmsd = &acad_vmstate; + adevc->build_dev_aml = build_acad_aml; +} + +static uint64_t acad_ioport_read(void *opaque, hwaddr addr, unsigned size) +{ + ACADState *s = opaque; + + acad_get_dynamic_status(s); + + switch (addr) { + case AC_STA_ADDR: + return s->state; + default: + g_assert_not_reached(); + } +} + +static const MemoryRegionOps acad_ops = { + .read = acad_ioport_read, + .impl = { + .min_access_size = 1, + .max_access_size = 1, + }, +}; + +static void acad_instance_init(Object *obj) +{ + ACADState *s = AC_ADAPTER_DEVICE(obj); + + memory_region_init_io(&s->io, obj, &acad_ops, s, "acad", + AC_ADAPTER_LEN); +} + +static const TypeInfo acad_info = { + .name = TYPE_AC_ADAPTER, + .parent = TYPE_ISA_DEVICE, + .instance_size = sizeof(ACADState), + .class_init = acad_class_init, + .instance_init = acad_instance_init, + .interfaces = (InterfaceInfo[]) { + { TYPE_ACPI_DEV_AML_IF }, + { }, + }, +}; + +static ACADState *find_acad_device(Error **errp) +{ + bool ambiguous; + Object *o = object_resolve_path_type("", TYPE_AC_ADAPTER, &ambiguous); + + if (!o) { + error_setg(errp, "No AC adapter device found"); + return NULL; + } + if (ambiguous) { + error_setg(errp, "More than one AC adapter device present"); + return NULL; + } + return AC_ADAPTER_DEVICE(o); +} + +void qmp_ac_adapter_set_state(bool connected, Error **errp) +{ + ACADState *s = find_acad_device(errp); + Object *obj; + + if (!s) { + return; + } + + s->qmp_connected = connected; + + obj = object_resolve_path_type("", TYPE_ACPI_DEVICE_IF, NULL); + if (obj) { + acpi_send_event(DEVICE(obj), ACPI_AC_ADAPTER_CHANGE_STATUS); + } +} + +AcAdapterInfo *qmp_query_ac_adapter(Error **errp) +{ + ACADState *s = find_acad_device(errp); + AcAdapterInfo *ret; + + if (!s) { + return NULL; + } + + ret = g_new0(AcAdapterInfo, 1); + ret->connected = s->qmp_connected; + + return ret; +} + +static void acad_register_types(void) +{ + type_register_static(&acad_info); +} + +type_init(acad_register_types) diff --git a/hw/acpi/meson.build b/hw/acpi/meson.build index e6bc78274e..731e9477e3 100644 --- a/hw/acpi/meson.build +++ b/hw/acpi/meson.build @@ -37,6 +37,8 @@ stub_ss.add(files('acpi-stub.c', 'aml-build-stub.c', 'ghes-stub.c')) stub_ss.add(files('pci-bridge-stub.c')) acpi_ss.add(when: 'CONFIG_BATTERY', if_true: files('battery.c')) stub_ss.add(files('battery-stub.c')) +acpi_ss.add(when: 'CONFIG_AC_ADAPTER', if_true: files('acad.c')) +stub_ss.add(files('acad-stub.c')) system_ss.add_all(when: 'CONFIG_ACPI', if_true: acpi_ss) system_ss.add(when: 'CONFIG_GHES_CPER', if_true: files('ghes_cper.c')) stub_ss.add(files('ghes_cper_stub.c')) diff --git a/hw/acpi/trace-events b/hw/acpi/trace-events index 8a6ab91a13..67602000f3 100644 --- a/hw/acpi/trace-events +++ b/hw/acpi/trace-events @@ -91,3 +91,7 @@ acpi_nvdimm_invalid_revision(uint32_t revision) "Revision 0x%" PRIx32 " is not s # battery.c battery_realize(void) "Battery device realize entry" battery_get_dynamic_status(uint32_t state, uint32_t rate, uint32_t charge) "Battery read state: 0x%"PRIx32", rate: %"PRIu32", charge: %"PRIu32 + +# acad.c +acad_realize(void) "AC adapter device realize entry" +acad_get_dynamic_status(uint8_t state) "AC adapter read state: %"PRIu8 diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig index 94004ffeb2..06f21cadb7 100644 --- a/hw/i386/Kconfig +++ b/hw/i386/Kconfig @@ -40,6 +40,7 @@ config PC imply NVDIMM imply FDC_ISA imply BATTERY + imply AC_ADAPTER select I8259 select I8254 select PCKBD diff --git a/include/hw/acpi/acad.h b/include/hw/acpi/acad.h new file mode 100644 index 0000000000..f163158f35 --- /dev/null +++ b/include/hw/acpi/acad.h @@ -0,0 +1,25 @@ +/* + * QEMU emulated AC adapter device. + * + * Copyright (c) 2019-2026 Janus Technologies, Inc. (http://janustech.com) + * + * Authors: + * Leonid Bloch <[email protected]> + * Marcel Apfelbaum <[email protected]> + * Dmitry Fleytman <[email protected]> + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + */ + +#ifndef HW_ACPI_AC_ADAPTER_H +#define HW_ACPI_AC_ADAPTER_H + +#define TYPE_AC_ADAPTER "acad" +#define AC_ADAPTER_IOPORT_PROP "ioport" + +#define AC_ADAPTER_VAL_UNKNOWN 0xFFFFFFFF + +#define AC_ADAPTER_LEN 1 + +#endif diff --git a/include/hw/acpi/acpi_dev_interface.h b/include/hw/acpi/acpi_dev_interface.h index a6f9022c0b..00566c56a7 100644 --- a/include/hw/acpi/acpi_dev_interface.h +++ b/include/hw/acpi/acpi_dev_interface.h @@ -15,6 +15,7 @@ typedef enum { ACPI_POWER_DOWN_STATUS = 64, ACPI_GENERIC_ERROR = 128, ACPI_BATTERY_CHANGE_STATUS = 256, + ACPI_AC_ADAPTER_CHANGE_STATUS = 2048, } AcpiEventStatusBits; #define TYPE_ACPI_DEVICE_IF "acpi-device-interface" diff --git a/qapi/acpi.json b/qapi/acpi.json index 4711a05614..025b5d8eaa 100644 --- a/qapi/acpi.json +++ b/qapi/acpi.json @@ -213,3 +213,50 @@ ## { 'command': 'query-battery', 'returns': 'BatteryInfo' } + +## +# @ac-adapter-set-state: +# +# Set the state of the emulated AC adapter device +# +# @connected: whether the AC adapter is connected +# +# Since: 11.1 +# +# .. qmp-example:: +# +# -> { "execute": "ac-adapter-set-state", +# "arguments": { "connected": true } } +# <- { "return": {} } +## +{ 'command': 'ac-adapter-set-state', + 'data': { 'connected': 'bool' } } + +## +# @AcAdapterInfo: +# +# AC adapter state information +# +# @connected: whether the AC adapter is connected +# +# Since: 11.1 +## +{ 'struct': 'AcAdapterInfo', + 'data': { 'connected': 'bool' } } + +## +# @query-ac-adapter: +# +# Query the current state of the emulated AC adapter device +# +# Returns: AC adapter connection state +# +# Since: 11.1 +# +# .. qmp-example:: +# +# -> { "execute": "query-ac-adapter" } +# <- { "return": { "connected": true } } +## +{ 'command': 'query-ac-adapter', + 'returns': 'AcAdapterInfo' } -- 2.54.0
