The lid button device communicates laptop lid state to the guest via ACPI.
Lid state is controlled programmatically via QMP commands for consistent
behavior across environments.

The device implements the ACPI_DEV_AML_IF interface to generate its
own AML code, placing the LID0 device directly under \_SB scope.

QMP commands:
- lid-button-set-state: Set lid open/closed state
- query-lid-button: Query current lid state

Signed-off-by: Leonid Bloch <[email protected]>
---
 MAINTAINERS                          |   2 +
 hw/acpi/Kconfig                      |   4 +
 hw/acpi/button-stub.c                |  20 +++
 hw/acpi/button.c                     | 227 +++++++++++++++++++++++++++
 hw/acpi/meson.build                  |   2 +
 hw/acpi/trace-events                 |   4 +
 hw/i386/Kconfig                      |   1 +
 include/hw/acpi/acpi_dev_interface.h |   1 +
 include/hw/acpi/button.h             |  23 +++
 qapi/acpi.json                       |  47 ++++++
 10 files changed, 331 insertions(+)
 create mode 100644 hw/acpi/button-stub.c
 create mode 100644 hw/acpi/button.c
 create mode 100644 include/hw/acpi/button.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 42179aba95..1f8f3e247e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3054,6 +3054,8 @@ Button
 M: Leonid Bloch <[email protected]>
 S: Maintained
 F: docs/specs/button.rst
+F: hw/acpi/button*
+F: include/hw/acpi/button.h
 
 Subsystems
 ----------
diff --git a/hw/acpi/Kconfig b/hw/acpi/Kconfig
index 889ace2dfa..0d5f885095 100644
--- a/hw/acpi/Kconfig
+++ b/hw/acpi/Kconfig
@@ -78,6 +78,10 @@ config AC_ADAPTER
     bool
     depends on ACPI
 
+config BUTTON
+    bool
+    depends on ACPI
+
 config ACPI_HW_REDUCED
     bool
     select ACPI
diff --git a/hw/acpi/button-stub.c b/hw/acpi/button-stub.c
new file mode 100644
index 0000000000..0ae478055b
--- /dev/null
+++ b/hw/acpi/button-stub.c
@@ -0,0 +1,20 @@
+/*
+ * QEMU emulated lid button 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_lid_button_set_state(bool open, Error **errp)
+{
+    error_setg(errp, "No lid button device found");
+}
+
+LidButtonInfo *qmp_query_lid_button(Error **errp)
+{
+    error_setg(errp, "No lid button device found");
+    return NULL;
+}
diff --git a/hw/acpi/button.c b/hw/acpi/button.c
new file mode 100644
index 0000000000..126969a5e7
--- /dev/null
+++ b/hw/acpi/button.c
@@ -0,0 +1,227 @@
+/*
+ * QEMU emulated lid button 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/button.h"
+
+#define BUTTON_DEVICE(obj) OBJECT_CHECK(ButtonState, (obj), \
+                                        TYPE_BUTTON)
+
+#define BUTTON_STA_ADDR            0
+
+enum {
+    LID_CLOSED = 0,
+    LID_OPEN = 1,
+};
+
+typedef struct ButtonState {
+    ISADevice dev;
+    MemoryRegion io;
+    uint16_t ioport;
+    uint8_t lid_state;
+    bool qmp_lid_open;
+} ButtonState;
+
+static void button_get_dynamic_status(ButtonState *s)
+{
+    trace_button_get_dynamic_status();
+
+    s->lid_state = s->qmp_lid_open ? LID_OPEN : LID_CLOSED;
+}
+
+static void button_realize(DeviceState *dev, Error **errp)
+{
+    ISADevice *d = ISA_DEVICE(dev);
+    ButtonState *s = BUTTON_DEVICE(dev);
+    bool ambiguous;
+
+    trace_button_realize();
+
+    object_resolve_path_type("", TYPE_BUTTON, &ambiguous);
+    if (ambiguous) {
+        error_setg(errp, "at most one %s device is permitted", TYPE_BUTTON);
+        return;
+    }
+
+    /* Initialize lid to open by default */
+    s->qmp_lid_open = true;
+
+    isa_register_ioport(d, &s->io, s->ioport);
+}
+
+static const Property button_device_properties[] = {
+    DEFINE_PROP_UINT16(BUTTON_IOPORT_PROP, ButtonState, ioport, 0x53d),
+};
+
+static const VMStateDescription button_vmstate = {
+    .name = "button",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (VMStateField[]) {
+        VMSTATE_BOOL(qmp_lid_open, ButtonState),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void build_button_aml(AcpiDevAmlIf *adev, Aml *scope)
+{
+    Aml *dev, *field, *method;
+    Aml *button_state;
+    Aml *sb_scope;
+    ButtonState *s = BUTTON_DEVICE(adev);
+
+    button_state = aml_local(0);
+
+    sb_scope = aml_scope("\\_SB");
+    dev = aml_device("LID0");
+    aml_append(dev, aml_name_decl("_HID", aml_string("PNP0C0D")));
+
+    aml_append(dev, aml_operation_region("LSTA", AML_SYSTEM_IO,
+                                         aml_int(s->ioport),
+                                         BUTTON_LEN));
+    field = aml_field("LSTA", AML_BYTE_ACC, AML_NOLOCK, AML_PRESERVE);
+    aml_append(field, aml_named_field("LIDS", 8));
+    aml_append(dev, field);
+
+    method = aml_method("_LID", 0, AML_NOTSERIALIZED);
+    aml_append(method, aml_store(aml_name("LIDS"), button_state));
+    aml_append(method, aml_return(button_state));
+    aml_append(dev, method);
+
+    aml_append(sb_scope, dev);
+    aml_append(scope, sb_scope);
+
+    /* Status Change */
+    method = aml_method("\\_GPE._E0C", 0, AML_NOTSERIALIZED);
+    aml_append(method, aml_notify(aml_name("\\_SB.LID0"), aml_int(0x80)));
+    aml_append(scope, method);
+}
+
+static void button_class_init(ObjectClass *class, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(class);
+    AcpiDevAmlIfClass *adevc = ACPI_DEV_AML_IF_CLASS(class);
+
+    dc->realize = button_realize;
+    dc->hotpluggable = false;
+    device_class_set_props(dc, button_device_properties);
+    dc->vmsd = &button_vmstate;
+    adevc->build_dev_aml = build_button_aml;
+}
+
+static uint64_t button_ioport_read(void *opaque, hwaddr addr, unsigned size)
+{
+    ButtonState *s = opaque;
+
+    button_get_dynamic_status(s);
+
+    switch (addr) {
+    case BUTTON_STA_ADDR:
+        return s->lid_state;
+    default:
+        g_assert_not_reached();
+    }
+}
+
+static const MemoryRegionOps button_ops = {
+    .read = button_ioport_read,
+    .impl = {
+        .min_access_size = 1,
+        .max_access_size = 1,
+    },
+};
+
+static void button_instance_init(Object *obj)
+{
+    ButtonState *s = BUTTON_DEVICE(obj);
+
+    memory_region_init_io(&s->io, obj, &button_ops, s, "button",
+                          BUTTON_LEN);
+}
+
+static const TypeInfo button_info = {
+    .name          = TYPE_BUTTON,
+    .parent        = TYPE_ISA_DEVICE,
+    .instance_size = sizeof(ButtonState),
+    .class_init    = button_class_init,
+    .instance_init = button_instance_init,
+    .interfaces = (InterfaceInfo[]) {
+        { TYPE_ACPI_DEV_AML_IF },
+        { },
+    },
+};
+
+static ButtonState *find_button_device(Error **errp)
+{
+    bool ambiguous;
+    Object *o = object_resolve_path_type("", TYPE_BUTTON, &ambiguous);
+
+    if (!o) {
+        error_setg(errp, "No lid button device found");
+        return NULL;
+    }
+    if (ambiguous) {
+        error_setg(errp, "More than one lid button device present");
+        return NULL;
+    }
+    return BUTTON_DEVICE(o);
+}
+
+void qmp_lid_button_set_state(bool open, Error **errp)
+{
+    ButtonState *s = find_button_device(errp);
+    Object *obj;
+
+    if (!s) {
+        return;
+    }
+
+    s->qmp_lid_open = open;
+
+    obj = object_resolve_path_type("", TYPE_ACPI_DEVICE_IF, NULL);
+    if (obj) {
+        acpi_send_event(DEVICE(obj), ACPI_BUTTON_CHANGE_STATUS);
+    }
+}
+
+LidButtonInfo *qmp_query_lid_button(Error **errp)
+{
+    ButtonState *s = find_button_device(errp);
+    LidButtonInfo *ret;
+
+    if (!s) {
+        return NULL;
+    }
+
+    ret = g_new0(LidButtonInfo, 1);
+    ret->open = s->qmp_lid_open;
+
+    return ret;
+}
+
+static void button_register_types(void)
+{
+    type_register_static(&button_info);
+}
+
+type_init(button_register_types)
diff --git a/hw/acpi/meson.build b/hw/acpi/meson.build
index 731e9477e3..ab0acd2521 100644
--- a/hw/acpi/meson.build
+++ b/hw/acpi/meson.build
@@ -39,6 +39,8 @@ 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'))
+acpi_ss.add(when: 'CONFIG_BUTTON', if_true: files('button.c'))
+stub_ss.add(files('button-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 67602000f3..13728637ce 100644
--- a/hw/acpi/trace-events
+++ b/hw/acpi/trace-events
@@ -95,3 +95,7 @@ battery_get_dynamic_status(uint32_t state, uint32_t rate, 
uint32_t charge) "Batt
 # acad.c
 acad_realize(void) "AC adapter device realize entry"
 acad_get_dynamic_status(uint8_t state) "AC adapter read state: %"PRIu8
+
+# button.c
+button_realize(void) "Button device realize entry"
+button_get_dynamic_status(void) "Button read dynamic status entry"
diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig
index 06f21cadb7..35c65d3f37 100644
--- a/hw/i386/Kconfig
+++ b/hw/i386/Kconfig
@@ -41,6 +41,7 @@ config PC
     imply FDC_ISA
     imply BATTERY
     imply AC_ADAPTER
+    imply BUTTON
     select I8259
     select I8254
     select PCKBD
diff --git a/include/hw/acpi/acpi_dev_interface.h 
b/include/hw/acpi/acpi_dev_interface.h
index 00566c56a7..35005d7ad0 100644
--- a/include/hw/acpi/acpi_dev_interface.h
+++ b/include/hw/acpi/acpi_dev_interface.h
@@ -16,6 +16,7 @@ typedef enum {
     ACPI_GENERIC_ERROR = 128,
     ACPI_BATTERY_CHANGE_STATUS = 256,
     ACPI_AC_ADAPTER_CHANGE_STATUS = 2048,
+    ACPI_BUTTON_CHANGE_STATUS = 4096,
 } AcpiEventStatusBits;
 
 #define TYPE_ACPI_DEVICE_IF "acpi-device-interface"
diff --git a/include/hw/acpi/button.h b/include/hw/acpi/button.h
new file mode 100644
index 0000000000..d0e2d33231
--- /dev/null
+++ b/include/hw/acpi/button.h
@@ -0,0 +1,23 @@
+/*
+ * QEMU emulated button 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_BUTTON_H
+#define HW_ACPI_BUTTON_H
+
+#define TYPE_BUTTON                  "button"
+#define BUTTON_IOPORT_PROP           "ioport"
+
+#define BUTTON_LEN                   1
+
+#endif
diff --git a/qapi/acpi.json b/qapi/acpi.json
index 025b5d8eaa..e0534e3657 100644
--- a/qapi/acpi.json
+++ b/qapi/acpi.json
@@ -260,3 +260,50 @@
 ##
 { 'command': 'query-ac-adapter',
   'returns': 'AcAdapterInfo' }
+
+##
+# @lid-button-set-state:
+#
+# Set the state of the emulated laptop lid button device
+#
+# @open: whether the lid is open
+#
+# Since: 11.1
+#
+# .. qmp-example::
+#
+#     -> { "execute": "lid-button-set-state",
+#          "arguments": { "open": true } }
+#     <- { "return": {} }
+##
+{ 'command': 'lid-button-set-state',
+  'data': { 'open': 'bool' } }
+
+##
+# @LidButtonInfo:
+#
+# Lid button state information
+#
+# @open: whether the lid is open
+#
+# Since: 11.1
+##
+{ 'struct': 'LidButtonInfo',
+  'data': { 'open': 'bool' } }
+
+##
+# @query-lid-button:
+#
+# Query the current state of the emulated laptop lid button device
+#
+# Returns: lid button state
+#
+# Since: 11.1
+#
+# .. qmp-example::
+#
+#     -> { "execute": "query-lid-button" }
+#     <- { "return": { "open": true } }
+##
+{ 'command': 'query-lid-button',
+  'returns': 'LidButtonInfo' }
-- 
2.54.0


Reply via email to