* [PATCH v2 0/4] Introduce a battery, AC adapter, and lid button
@ 2025-08-21 17:45 Leonid Bloch
2025-08-21 17:45 ` [PATCH v2 1/4] hw/acpi: Increase the number of possible ACPI interrupts Leonid Bloch
` (4 more replies)
0 siblings, 5 replies; 7+ messages in thread
From: Leonid Bloch @ 2025-08-21 17:45 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
First, I must apologize for the extremely long delay since v1 - it has been
nearly 4 years, which is unacceptable. Life circumstances intervened.
This series introduces three ACPI devices that are particularly useful
for laptop/mobile virtualization:
* Battery
* AC adapter
* Laptop lid button
Changes in v2:
--------------
Based on the feedback from Philippe Mathieu-Daudé and Michael S. Tsirkin:
* Complete redesign with dual-mode operation:
- QMP control mode (default): Devices are controlled via QMP commands,
providing deterministic behavior essential for migration and CI/testing
- Host mirroring mode (opt-in): Original sysfs/procfs monitoring behavior,
now disabled by default
* Migrated to modern QEMU ACPI architecture:
- Devices now implement ACPI_DEV_AML_IF interface
- AML generation moved from centralized acpi-build.c to device files
* Added a QMP interface:
- battery-set-state/query-battery
- ac-adapter-set-state/query-ac-adapter
- lid-button-set-state/query-lid-button
* Documentation improvements:
- Converted to .rst format
- Added examples for both QMP and "fake" sysfs/procfs testing
The dual-mode design ensures these devices are migration-safe and
deterministic by default, while still allowing host state mirroring
when explicitly requested for desktop use cases.
Use cases:
----------
1. Testing: CI systems can programmatically control power states
2. Cloud: Expose virtual battery for usage-based resource limiting
3. Desktop virtualization: Mirror host laptop state to guest (opt-in)
4. Development: Test power management without physical hardware
Example usage:
--------------
# Default QMP-controlled battery
qemu-system-x86_64 -device battery
# Mirror host battery
qemu-system-x86_64 -device battery,use-qmp=false,enable-sysfs=true
# Control via QMP
{"execute": "battery-set-state",
"arguments": {"state": {"present": true, "charging": false,
"discharging": true, "charge-percent": 42,
"rate": 500}}}
The series has been tested with Windows and Linux guests, correctly
showing battery status, AC adapter state, and lid button events in
guest UIs and triggering appropriate power management actions.
Thanks again for your patience and feedback.
Leonid.
Leonid Bloch (4):
hw/acpi: Increase the number of possible ACPI interrupts
hw/acpi: Introduce the QEMU Battery
hw/acpi: Introduce the QEMU AC adapter
hw/acpi: Introduce the QEMU lid button
MAINTAINERS | 18 +
docs/specs/acad.rst | 195 +++++++
docs/specs/battery.rst | 225 ++++++++
docs/specs/button.rst | 189 +++++++
docs/specs/index.rst | 3 +
hw/acpi/Kconfig | 12 +
hw/acpi/acad.c | 447 ++++++++++++++++
hw/acpi/battery.c | 735 +++++++++++++++++++++++++++
hw/acpi/button.c | 438 ++++++++++++++++
hw/acpi/core.c | 17 +-
hw/acpi/meson.build | 3 +
hw/acpi/trace-events | 15 +
hw/i386/Kconfig | 3 +
hw/i386/acpi-build.c | 1 +
include/hw/acpi/acad.h | 27 +
include/hw/acpi/acpi_dev_interface.h | 3 +
include/hw/acpi/battery.h | 33 ++
include/hw/acpi/button.h | 25 +
qapi/acpi.json | 171 +++++++
19 files changed, 2558 insertions(+), 2 deletions(-)
create mode 100644 docs/specs/acad.rst
create mode 100644 docs/specs/battery.rst
create mode 100644 docs/specs/button.rst
create mode 100644 hw/acpi/acad.c
create mode 100644 hw/acpi/battery.c
create mode 100644 hw/acpi/button.c
create mode 100644 include/hw/acpi/acad.h
create mode 100644 include/hw/acpi/battery.h
create mode 100644 include/hw/acpi/button.h
--
2.50.1
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v2 1/4] hw/acpi: Increase the number of possible ACPI interrupts
2025-08-21 17:45 [PATCH v2 0/4] Introduce a battery, AC adapter, and lid button Leonid Bloch
@ 2025-08-21 17:45 ` Leonid Bloch
2025-08-26 12:47 ` Igor Mammedov
2025-08-21 17:45 ` [PATCH v2 2/4] hw/acpi: Introduce the QEMU Battery Leonid Bloch
` (3 subsequent siblings)
4 siblings, 1 reply; 7+ messages in thread
From: Leonid Bloch @ 2025-08-21 17:45 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
Increase the number of possible ACPI interrupts from 8, to the maximum
available: 64 by default.
Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
---
hw/acpi/core.c | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/hw/acpi/core.c b/hw/acpi/core.c
index 58f8964e13..447ff07878 100644
--- a/hw/acpi/core.c
+++ b/hw/acpi/core.c
@@ -729,19 +729,32 @@ uint32_t acpi_gpe_ioport_readb(ACPIREGS *ar, uint32_t addr)
void acpi_send_gpe_event(ACPIREGS *ar, qemu_irq irq,
AcpiEventStatusBits status)
{
- ar->gpe.sts[0] |= status;
+ int i;
+
+ AcpiEventStatusBits st = status;
+ for (i = 0; i < ar->gpe.len / 2; i++) {
+ ar->gpe.sts[i] |= st;
+ st >>= (sizeof(ar->gpe.sts[0]) * CHAR_BIT);
+ }
+
acpi_update_sci(ar, irq);
}
void acpi_update_sci(ACPIREGS *regs, qemu_irq irq)
{
int sci_level, pm1a_sts;
+ uint64_t gpe_sci = 0;
+ int i;
pm1a_sts = acpi_pm1_evt_get_sts(regs);
+ for (i = 0; i < regs->gpe.len / 2; i++) {
+ gpe_sci |= (regs->gpe.sts[i] & regs->gpe.en[i]);
+ }
+
sci_level = ((pm1a_sts &
regs->pm1.evt.en & ACPI_BITMASK_PM1_COMMON_ENABLED) != 0) ||
- ((regs->gpe.sts[0] & regs->gpe.en[0]) != 0);
+ (gpe_sci != 0);
qemu_set_irq(irq, sci_level);
--
2.50.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v2 2/4] hw/acpi: Introduce the QEMU Battery
2025-08-21 17:45 [PATCH v2 0/4] Introduce a battery, AC adapter, and lid button Leonid Bloch
2025-08-21 17:45 ` [PATCH v2 1/4] hw/acpi: Increase the number of possible ACPI interrupts Leonid Bloch
@ 2025-08-21 17:45 ` Leonid Bloch
2025-08-21 17:45 ` [PATCH v2 3/4] hw/acpi: Introduce the QEMU AC adapter Leonid Bloch
` (2 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Leonid Bloch @ 2025-08-21 17:45 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
The battery device communicates battery state to the guest via ACPI.
It supports two modes of operation:
1. QMP control mode (default): Battery state is controlled programmatically
via QMP commands, making the device deterministic and migration-safe.
2. Host mirroring mode (optional): The device reflects the host's battery
state from sysfs. Probing occurs on guest ACPI requests and at timed
intervals (default 2 seconds, configurable via 'probe_interval' property
in milliseconds). State changes trigger ACPI notifications to the guest.
Properties:
- 'use-qmp': Enable QMP control mode (default: true)
- 'enable-sysfs': Enable host battery mirroring (default: false)
- 'probe_interval': Probe interval in ms for sysfs mode (default: 2000)
- 'sysfs_path': Override default sysfs path /sys/class/power_supply/
The device implements the ACPI_DEV_AML_IF interface to generate its
own AML code, placing the BAT0 device directly under \_SB scope as
per ACPI specification.
QMP commands:
- battery-set-state: Set battery state (present, charging, capacity, rate)
- query-battery: Query current battery state
This allows testing without host battery access and provides a stable
interface for virtualization management systems.
Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
Signed-off-by: Marcel Apfelbaum <marcel.apfelbaum@gmail.com>
---
MAINTAINERS | 6 +
docs/specs/battery.rst | 225 ++++++++
docs/specs/index.rst | 1 +
hw/acpi/Kconfig | 4 +
hw/acpi/battery.c | 735 +++++++++++++++++++++++++++
hw/acpi/meson.build | 1 +
hw/acpi/trace-events | 5 +
hw/i386/Kconfig | 1 +
hw/i386/acpi-build.c | 1 +
include/hw/acpi/acpi_dev_interface.h | 1 +
include/hw/acpi/battery.h | 33 ++
qapi/acpi.json | 73 +++
12 files changed, 1086 insertions(+)
create mode 100644 docs/specs/battery.rst
create mode 100644 hw/acpi/battery.c
create mode 100644 include/hw/acpi/battery.h
diff --git a/MAINTAINERS b/MAINTAINERS
index a07086ed76..51af9b7366 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2921,6 +2921,12 @@ F: hw/vmapple/*
F: include/hw/vmapple/*
F: docs/system/arm/vmapple.rst
+Battery
+M: Leonid Bloch <lb.workbox@gmail.com>
+S: Maintained
+F: hw/acpi/battery.*
+F: docs/specs/battery.rst
+
Subsystems
----------
Overall Audio backends
diff --git a/docs/specs/battery.rst b/docs/specs/battery.rst
new file mode 100644
index 0000000000..e426b91ea5
--- /dev/null
+++ b/docs/specs/battery.rst
@@ -0,0 +1,225 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+==============
+Battery Device
+==============
+
+The battery device provides battery state information to the guest. It supports
+two operating modes:
+
+1. **QMP Control Mode** (default): Battery state is controlled via QMP commands,
+ providing deterministic control for testing and migration safety.
+2. **Sysfs Mode**: Battery state mirrors the host's physical battery, useful
+ for desktop virtualization where the guest should see the host's battery.
+
+Configuration
+-------------
+
+The battery device is created as an ISA device using ``-device battery``.
+
+Operating Modes
+~~~~~~~~~~~~~~~
+
+**QMP Control Mode** (``use-qmp=true``, default)
+ Battery state is controlled via QMP commands. This mode is recommended for:
+
+ * Production environments requiring migration support
+ * Testing with predictable battery states
+ * Environments without host battery access
+ * Security-sensitive deployments
+
+**Sysfs Mode** (``enable-sysfs=true``)
+ Battery mirrors the host's physical battery. This mode is useful for:
+
+ * Desktop virtualization on laptops
+ * Development and testing with real battery behavior
+
+ Note: Sysfs mode reads host files and runs timers, which may impact
+ security and migration. Use with caution in production.
+
+Properties
+~~~~~~~~~~
+
+``ioport`` (default: 0x530)
+ I/O port base address for the battery device registers.
+
+``use-qmp`` (default: true)
+ Enable QMP control mode. When true, battery state is controlled via
+ QMP commands. Cannot be used together with ``enable-sysfs=true``.
+
+``enable-sysfs`` (default: false)
+ Enable sysfs mode to mirror the host's battery. Cannot be used together
+ with ``use-qmp=true``.
+
+``probe_interval`` (default: 2000)
+ Time interval between periodic probes in milliseconds (sysfs mode only).
+ A zero value disables the periodic probes, and makes the battery state
+ updates occur on guest requests only.
+
+``sysfs_path`` (default: auto-detected)
+ Path to the host's battery sysfs directory (sysfs mode only). If not
+ specified, the device will automatically detect the battery from
+ ``/sys/class/power_supply/``. This property allows overriding the default
+ path if:
+
+ * The sysfs path differs from the standard location
+ * A different battery needs to be probed
+ * A "fake" host battery is to be provided for testing
+
+Host Battery Detection
+----------------------
+
+The host's battery information is taken from the sysfs battery data,
+located in::
+
+ /sys/class/power_supply/[device of type "Battery"]
+
+The device automatically scans for the first available battery device
+with type "Battery" in the power_supply directory.
+
+ACPI Interface
+--------------
+
+The battery device exposes itself as an ACPI battery device with:
+
+* **_HID**: ``PNP0C0A`` (Control Method Battery)
+* **Device path**: ``\_SB.BAT0``
+
+The device implements standard ACPI battery methods:
+
+``_STA`` (Status)
+ Returns the battery presence status.
+
+``_BIF`` (Battery Information)
+ Returns static battery information including design capacity,
+ technology, and model information.
+
+``_BST`` (Battery Status)
+ Returns dynamic battery status including current state
+ (charging/discharging), present rate, and remaining capacity.
+
+I/O Interface
+-------------
+
+The battery device exposes 12 bytes of I/O space at the configured
+I/O port address with the following layout:
+
+* **Bytes 0-3**: Battery state (DWORD)
+
+ * 1 = Discharging
+ * 2 = Charging
+
+* **Bytes 4-7**: Battery rate (DWORD)
+
+ Current charge/discharge rate normalized to design capacity.
+
+* **Bytes 8-11**: Battery charge (DWORD)
+
+ Current battery charge level normalized to design capacity.
+
+All values are normalized where the design capacity equals 10000 units.
+Unknown values are represented as 0xFFFFFFFF.
+
+ACPI Notifications
+------------------
+
+The battery device generates ACPI notifications through GPE events:
+
+* **GPE._E07**: Device Check (0x01) - Battery insertion/removal
+* **GPE._E08**: Status Change (0x80) - Battery state change
+* **GPE._E09**: Information Change (0x81) - Battery information update
+
+QMP Commands
+------------
+
+When using QMP control mode (default), the following commands are available:
+
+``battery-set-state``
+ Set the battery state. Parameters:
+
+ * ``present``: Whether the battery is present (boolean)
+ * ``charging``: Whether the battery is charging (boolean)
+ * ``discharging``: Whether the battery is discharging (boolean)
+ * ``charge-percent``: Battery charge percentage 0-100 (integer)
+ * ``rate``: Charge/discharge rate in mW (optional integer)
+
+ Example::
+
+ -> { "execute": "battery-set-state",
+ "arguments": { "state": {
+ "present": true,
+ "charging": true,
+ "discharging": false,
+ "charge-percent": 85,
+ "rate": 500
+ }}}
+ <- { "return": {} }
+
+``query-battery``
+ Query the current battery state. Returns the same fields as above.
+
+ Example::
+
+ -> { "execute": "query-battery" }
+ <- { "return": {
+ "present": true,
+ "charging": true,
+ "discharging": false,
+ "charge-percent": 85,
+ "rate": 500,
+ "design-capacity": 10000
+ }}
+
+Examples
+--------
+
+QMP control mode (default - recommended)::
+
+ # Start with QMP control
+ qemu-system-x86_64 -device battery -qmp tcp:localhost:4444,server,wait=off
+
+ # From another terminal, set battery state via QMP:
+ echo '{"execute":"qmp_capabilities"}
+ {"execute":"battery-set-state",
+ "arguments":{"state":{"present":true,"charging":false,
+ "discharging":true,"charge-percent":42,
+ "rate":500}}}' | \
+ nc -N localhost 4444
+
+Sysfs mode (mirror host battery)::
+
+ # Enable sysfs mode to mirror host battery
+ qemu-system-x86_64 -device battery,use-qmp=false,enable-sysfs=true
+
+ # Custom probe interval (5 seconds)
+ qemu-system-x86_64 -device battery,use-qmp=false,enable-sysfs=true,probe_interval=5000
+
+ # Specific battery path
+ qemu-system-x86_64 -device battery,use-qmp=false,enable-sysfs=true,sysfs_path=/sys/class/power_supply/BAT1
+
+Testing with fake battery::
+
+ # Create fake battery files for testing
+ mkdir -p /tmp/fake_battery
+ echo "Battery" > /tmp/fake_battery/type
+ echo "Discharging" > /tmp/fake_battery/status
+ echo "50" > /tmp/fake_battery/capacity
+ echo "1500000" > /tmp/fake_battery/energy_now # Current energy in μWh
+ echo "3000000" > /tmp/fake_battery/energy_full # Full capacity in μWh
+ echo "500000" > /tmp/fake_battery/power_now # Current power in μW
+
+ # Use fake battery in sysfs mode
+ qemu-system-x86_64 -device battery,use-qmp=false,enable-sysfs=true,sysfs_path=/tmp/fake_battery
+
+ # Update battery state while VM is running (from another terminal)
+ # Change to 75% charging:
+ echo "Charging" > /tmp/fake_battery/status
+ echo "75" > /tmp/fake_battery/capacity
+ echo "2250000" > /tmp/fake_battery/energy_now # 75% of 3000000
+ echo "500000" > /tmp/fake_battery/power_now # Charging rate (500 mW)
+
+ # Change to 25% discharging:
+ echo "Discharging" > /tmp/fake_battery/status
+ echo "25" > /tmp/fake_battery/capacity
+ echo "750000" > /tmp/fake_battery/energy_now # 25% of 3000000
+ echo "300000" > /tmp/fake_battery/power_now # Discharge rate (300 mW)
diff --git a/docs/specs/index.rst b/docs/specs/index.rst
index f19d73c9f6..616e8228cc 100644
--- a/docs/specs/index.rst
+++ b/docs/specs/index.rst
@@ -22,6 +22,7 @@ guest hardware that is specific to QEMU.
acpi_pci_hotplug
acpi_nvdimm
acpi_erst
+ battery
sev-guest-firmware
fw_cfg
fsi
diff --git a/hw/acpi/Kconfig b/hw/acpi/Kconfig
index 1d4e9f0845..64403378bd 100644
--- a/hw/acpi/Kconfig
+++ b/hw/acpi/Kconfig
@@ -83,3 +83,7 @@ config ACPI_ERST
config ACPI_CXL
bool
depends on ACPI
+
+config BATTERY
+ bool
+ depends on ACPI
diff --git a/hw/acpi/battery.c b/hw/acpi/battery.c
new file mode 100644
index 0000000000..5ab3ab7472
--- /dev/null
+++ b/hw/acpi/battery.c
@@ -0,0 +1,735 @@
+/*
+ * QEMU emulated battery device.
+ *
+ * Copyright (c) 2019 Janus Technologies, Inc. (http://janustech.com)
+ *
+ * Authors:
+ * Leonid Bloch <lb.workbox@gmail.com>
+ * Marcel Apfelbaum <marcel.apfelbaum@gmail.com>
+ * Dmitry Fleytman <dmitry.fleytman@gmail.com>
+ *
+ * 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 "hw/nvram/fw_cfg.h"
+#include "qapi/error.h"
+#include "qemu/error-report.h"
+#include "hw/qdev-properties.h"
+#include "migration/vmstate.h"
+#include "hw/acpi/acpi_aml_interface.h"
+#include "qapi/qapi-commands-acpi.h"
+
+#include "hw/acpi/battery.h"
+
+#define BATTERY_DEVICE(obj) OBJECT_CHECK(BatteryState, (obj), TYPE_BATTERY)
+
+#define BATTERY_DISCHARGING 0x01 /* ACPI _BST bit 0 */
+#define BATTERY_CHARGING 0x02 /* ACPI _BST bit 1 */
+#define BATTERY_CRITICAL 0x04 /* ACPI _BST bit 2 */
+
+#define SYSFS_PATH "/sys/class/power_supply"
+#define BATTERY_TYPE "Battery"
+
+#define MAX_ALLOWED_STATE_LENGTH 32 /* For convinience when comparing */
+
+#define NORMALIZE_BY_FULL(val, full) \
+ ((full == 0) ? BATTERY_VAL_UNKNOWN \
+ : (uint32_t)(val * BATTERY_FULL_CAP / full))
+
+typedef union bat_metric {
+ uint32_t val;
+ uint8_t acc[4];
+} bat_metric;
+
+typedef struct BatteryState {
+ ISADevice dev;
+ MemoryRegion io;
+ uint16_t ioport;
+ bat_metric state;
+ bat_metric rate;
+ bat_metric charge;
+ uint32_t charge_full;
+ int units; /* 0 - mWh, 1 - mAh */
+ bool use_qmp_control;
+ bool qmp_present;
+ bool qmp_charging;
+ bool qmp_discharging;
+ int qmp_charge_percent;
+ int qmp_rate;
+ bool enable_sysfs;
+
+ QEMUTimer *probe_state_timer;
+ uint64_t probe_state_interval;
+
+ char *bat_path;
+} BatteryState;
+
+/* Access addresses */
+enum acc_addr {
+ bsta_addr0, bsta_addr1, bsta_addr2, bsta_addr3,
+ brte_addr0, brte_addr1, brte_addr2, brte_addr3,
+ bcrg_addr0, bcrg_addr1, bcrg_addr2, bcrg_addr3
+};
+
+/* Files used when the units are: mWh mAh */
+static const char *full_file[] = { "energy_full", "charge_full" };
+static const char *now_file[] = { "energy_now", "charge_now" };
+static const char *rate_file[] = { "power_now", "current_now" };
+
+static const char *stat_file = "status";
+static const char *type_file = "type";
+
+static const char *discharging_states[] = { "Discharging", "Not charging" };
+static const char *charging_states[] = { "Charging", "Full", "Unknown" };
+
+static inline bool battery_file_accessible(char *path, const char *file)
+{
+ char full_path[PATH_MAX];
+ int path_len;
+
+ path_len = snprintf(full_path, PATH_MAX, "%s/%s", path, file);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ return false;
+ }
+ if (access(full_path, R_OK) == 0) {
+ return true;
+ }
+ return false;
+}
+
+static inline int battery_select_file(char *path, const char **file)
+{
+ if (battery_file_accessible(path, file[0])) {
+ return 0;
+ } else if (battery_file_accessible(path, file[1])) {
+ return 1;
+ } else {
+ return -1;
+ }
+}
+
+static void battery_get_full_charge(BatteryState *s, Error **errp)
+{
+ char file_path[PATH_MAX];
+ int path_len;
+ uint32_t val;
+ FILE *ff;
+
+ path_len = snprintf(file_path, PATH_MAX, "%s/%s", s->bat_path,
+ full_file[s->units]);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ error_setg(errp, "Full capacity file path is inaccessible.");
+ return;
+ }
+
+ ff = fopen(file_path, "r");
+ if (ff == NULL) {
+ error_setg_errno(errp, errno, "Could not read the full charge file.");
+ return;
+ }
+
+ if (fscanf(ff, "%u", &val) != 1) {
+ error_setg(errp, "Full capacity undetermined.");
+ return;
+ } else {
+ s->charge_full = val;
+ }
+ fclose(ff);
+}
+
+static inline bool battery_is_discharging(char *val)
+{
+ static const int discharging_len = ARRAY_SIZE(discharging_states);
+ int i;
+
+ for (i = 0; i < discharging_len; i++) {
+ if (!strncmp(val, discharging_states[i], MAX_ALLOWED_STATE_LENGTH)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static inline bool battery_is_charging(char *val)
+{
+ static const int charging_len = ARRAY_SIZE(charging_states);
+ int i;
+
+ for (i = 0; i < charging_len; i++) {
+ if (!strncmp(val, charging_states[i], MAX_ALLOWED_STATE_LENGTH)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static void battery_get_state(BatteryState *s)
+{
+ char file_path[PATH_MAX];
+ int path_len;
+ char val[MAX_ALLOWED_STATE_LENGTH];
+ FILE *ff;
+
+ path_len = snprintf(file_path, PATH_MAX, "%s/%s", s->bat_path, stat_file);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ warn_report("Could not read the battery state.");
+ return;
+ }
+
+ ff = fopen(file_path, "r");
+ if (ff == NULL) {
+ warn_report("Could not read the battery state.");
+ return;
+ }
+
+ if (fgets(val, MAX_ALLOWED_STATE_LENGTH, ff) == NULL) {
+ warn_report("Battery state unreadable.");
+ } else {
+ val[strcspn(val, "\n")] = 0;
+ if (battery_is_discharging(val)) {
+ s->state.val = BATTERY_DISCHARGING;
+ } else if (battery_is_charging(val)) {
+ s->state.val = BATTERY_CHARGING;
+ } else {
+ s->state.val = 0;
+ warn_report("Battery state undetermined.");
+ }
+ }
+ fclose(ff);
+}
+
+static void battery_get_rate(BatteryState *s)
+{
+ char file_path[PATH_MAX];
+ int path_len;
+ uint64_t val;
+ FILE *ff;
+
+ path_len = snprintf(file_path, PATH_MAX, "%s/%s", s->bat_path,
+ rate_file[s->units]);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ warn_report("Could not read the battery rate.");
+ s->rate.val = BATTERY_VAL_UNKNOWN;
+ return;
+ }
+
+ ff = fopen(file_path, "r");
+ if (ff == NULL) {
+ warn_report("Could not read the battery rate.");
+ s->rate.val = BATTERY_VAL_UNKNOWN;
+ return;
+ }
+
+ if (fscanf(ff, "%lu", &val) != 1) {
+ warn_report("Battery rate undetermined.");
+ s->rate.val = BATTERY_VAL_UNKNOWN;
+ } else {
+ s->rate.val = NORMALIZE_BY_FULL(val, s->charge_full);
+ }
+ fclose(ff);
+}
+
+static void battery_get_charge(BatteryState *s)
+{
+ char file_path[PATH_MAX];
+ int path_len;
+ uint64_t val;
+ FILE *ff;
+
+ path_len = snprintf(file_path, PATH_MAX, "%s/%s", s->bat_path,
+ now_file[s->units]);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ warn_report("Could not read the battery charge.");
+ s->charge.val = BATTERY_VAL_UNKNOWN;
+ return;
+ }
+
+ ff = fopen(file_path, "r");
+ if (ff == NULL) {
+ warn_report("Could not read the battery charge.");
+ s->charge.val = BATTERY_VAL_UNKNOWN;
+ return;
+ }
+
+ if (fscanf(ff, "%lu", &val) != 1) {
+ warn_report("Battery charge undetermined.");
+ s->charge.val = BATTERY_VAL_UNKNOWN;
+ } else {
+ s->charge.val = NORMALIZE_BY_FULL(val, s->charge_full);
+ }
+ fclose(ff);
+}
+
+static void battery_get_dynamic_status(BatteryState *s)
+{
+ if (s->use_qmp_control) {
+ s->state.val = 0;
+ if (s->qmp_present) {
+ if (s->qmp_charging) {
+ s->state.val |= BATTERY_CHARGING;
+ }
+ if (s->qmp_discharging) {
+ s->state.val |= BATTERY_DISCHARGING;
+ }
+ }
+ s->rate.val = s->qmp_rate;
+ s->charge.val = (s->qmp_charge_percent * BATTERY_FULL_CAP) / 100;
+ } else if (s->enable_sysfs) {
+ battery_get_state(s);
+ battery_get_rate(s);
+ battery_get_charge(s);
+ } else {
+ s->state.val = 0;
+ s->rate.val = 0;
+ s->charge.val = 0;
+ }
+
+ trace_battery_get_dynamic_status(s->state.val, s->rate.val, s->charge.val);
+}
+
+static void battery_probe_state(void *opaque)
+{
+ BatteryState *s = opaque;
+
+ uint32_t state_before = s->state.val;
+ uint32_t rate_before = s->rate.val;
+ uint32_t charge_before = s->charge.val;
+
+ battery_get_dynamic_status(s);
+
+ if (state_before != s->state.val || rate_before != s->rate.val ||
+ charge_before != s->charge.val) {
+ Object *obj = object_resolve_path_type("", TYPE_ACPI_DEVICE_IF, NULL);
+ switch (charge_before) {
+ case 0:
+ break; /* Avoid marking initiation as an update */
+ default:
+ acpi_send_event(DEVICE(obj), ACPI_BATTERY_CHANGE_STATUS);
+ }
+ }
+ timer_mod(s->probe_state_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) +
+ s->probe_state_interval);
+}
+
+static void battery_probe_state_timer_init(BatteryState *s)
+{
+ if (s->enable_sysfs && s->probe_state_interval > 0) {
+ s->probe_state_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
+ battery_probe_state, s);
+ timer_mod(s->probe_state_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) +
+ s->probe_state_interval);
+ }
+}
+
+static bool battery_verify_sysfs(BatteryState *s, char *path)
+{
+ int units;
+ FILE *ff;
+ char type_path[PATH_MAX];
+ int path_len;
+ char val[MAX_ALLOWED_STATE_LENGTH];
+
+ path_len = snprintf(type_path, PATH_MAX, "%s/%s", path, type_file);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ return false;
+ }
+ ff = fopen(type_path, "r");
+ if (ff == NULL) {
+ return false;
+ }
+
+ if (fgets(val, MAX_ALLOWED_STATE_LENGTH, ff) == NULL) {
+ fclose(ff);
+ return false;
+ } else {
+ val[strcspn(val, "\n")] = 0;
+ if (strncmp(val, BATTERY_TYPE, MAX_ALLOWED_STATE_LENGTH)) {
+ fclose(ff);
+ return false;
+ }
+ }
+ fclose(ff);
+
+ units = battery_select_file(path, full_file);
+
+ if (units < 0) {
+ return false;
+ } else {
+ s->units = units;
+ }
+
+ return (battery_file_accessible(path, now_file[s->units])
+ & battery_file_accessible(path, rate_file[s->units])
+ & battery_file_accessible(path, stat_file));
+}
+
+static bool get_battery_path(DeviceState *dev)
+{
+ BatteryState *s = BATTERY_DEVICE(dev);
+ DIR *dir;
+ struct dirent *ent;
+ char bp[PATH_MAX];
+ int path_len;
+
+ if (s->bat_path) {
+ return battery_verify_sysfs(s, s->bat_path);
+ }
+
+ dir = opendir(SYSFS_PATH);
+ if (dir == NULL) {
+ return false;
+ }
+
+ ent = readdir(dir);
+ while (ent != NULL) {
+ if (ent->d_name[0] != '.') {
+ path_len = snprintf(bp, PATH_MAX, "%s/%s", SYSFS_PATH,
+ ent->d_name);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ return false;
+ }
+ if (battery_verify_sysfs(s, bp)) {
+ qdev_prop_set_string(dev, BATTERY_PATH_PROP, bp);
+ closedir(dir);
+ return true;
+ }
+ }
+ ent = readdir(dir);
+ }
+ closedir(dir);
+
+ return false;
+}
+
+static void battery_realize(DeviceState *dev, Error **errp)
+{
+ ISADevice *d = ISA_DEVICE(dev);
+ BatteryState *s = BATTERY_DEVICE(dev);
+ FWCfgState *fw_cfg = fw_cfg_find();
+ uint16_t *battery_port;
+ char err_details[0x20] = {};
+
+ trace_battery_realize();
+
+ if (s->use_qmp_control && s->enable_sysfs) {
+ error_setg(errp, "Cannot enable both QMP control and sysfs mode");
+ return;
+ }
+
+ /* Initialize QMP state to sensible defaults when in QMP mode */
+ if (s->use_qmp_control) {
+ s->qmp_present = true;
+ s->qmp_charging = false;
+ s->qmp_discharging = true;
+ s->qmp_charge_percent = 50;
+ s->qmp_rate = 1000; /* 1000 mW discharge rate */
+ }
+
+ if (s->enable_sysfs) {
+ if (!s->bat_path) {
+ strcpy(err_details, " Try using 'sysfs_path='");
+ }
+
+ if (!get_battery_path(dev)) {
+ error_setg(errp, "Battery sysfs path not found or unreadable.%s",
+ err_details);
+ return;
+ }
+ battery_get_full_charge(s, errp);
+ } else {
+ s->charge_full = BATTERY_FULL_CAP;
+ }
+
+ isa_register_ioport(d, &s->io, s->ioport);
+
+ battery_probe_state_timer_init(s);
+
+ if (!fw_cfg) {
+ return;
+ }
+
+ battery_port = g_malloc(sizeof(*battery_port));
+ *battery_port = cpu_to_le16(s->ioport);
+ fw_cfg_add_file(fw_cfg, "etc/battery-port", battery_port,
+ sizeof(*battery_port));
+}
+
+static const Property battery_device_properties[] = {
+ DEFINE_PROP_UINT16(BATTERY_IOPORT_PROP, BatteryState, ioport, 0x530),
+ DEFINE_PROP_BOOL("use-qmp", BatteryState, use_qmp_control, true),
+ DEFINE_PROP_BOOL("enable-sysfs", BatteryState, enable_sysfs, false),
+ DEFINE_PROP_UINT64(BATTERY_PROBE_STATE_INTERVAL, BatteryState,
+ probe_state_interval, 2000),
+ DEFINE_PROP_STRING(BATTERY_PATH_PROP, BatteryState, bat_path),
+};
+
+static const VMStateDescription battery_vmstate = {
+ .name = "battery",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (VMStateField[]) {
+ VMSTATE_UINT16(ioport, BatteryState),
+ VMSTATE_UINT64(probe_state_interval, BatteryState),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static void build_battery_aml(AcpiDevAmlIf *adev, Aml *scope)
+{
+ Aml *dev, *field, *method, *pkg;
+ Aml *bat_state, *bat_rate, *bat_charge;
+ Aml *sb_scope;
+ BatteryState *s = BATTERY_DEVICE(adev);
+
+ bat_state = aml_local(0);
+ bat_rate = aml_local(1);
+ bat_charge = aml_local(2);
+
+ sb_scope = aml_scope("\\_SB");
+ dev = aml_device("BAT0");
+ aml_append(dev, aml_name_decl("_HID", aml_eisaid("PNP0C0A")));
+
+ method = aml_method("_STA", 0, AML_NOTSERIALIZED);
+ aml_append(method, aml_return(aml_int(0x1F)));
+ aml_append(dev, method);
+
+ aml_append(dev, aml_operation_region("DBST", AML_SYSTEM_IO,
+ aml_int(s->ioport),
+ BATTERY_LEN));
+ field = aml_field("DBST", AML_DWORD_ACC, AML_NOLOCK, AML_PRESERVE);
+ aml_append(field, aml_named_field("BSTA", 32));
+ aml_append(field, aml_named_field("BRTE", 32));
+ aml_append(field, aml_named_field("BCRG", 32));
+ aml_append(dev, field);
+
+ method = aml_method("_BIF", 0, AML_NOTSERIALIZED);
+ pkg = aml_package(13);
+ /* Power Unit */
+ aml_append(pkg, aml_int(0)); /* mW */
+ /* Design Capacity */
+ aml_append(pkg, aml_int(BATTERY_FULL_CAP));
+ /* Last Full Charge Capacity */
+ aml_append(pkg, aml_int(BATTERY_FULL_CAP));
+ /* Battery Technology */
+ aml_append(pkg, aml_int(1)); /* Secondary */
+ /* Design Voltage */
+ aml_append(pkg, aml_int(BATTERY_VAL_UNKNOWN));
+ /* Design Capacity of Warning */
+ aml_append(pkg, aml_int(BATTERY_CAPACITY_OF_WARNING));
+ /* Design Capacity of Low */
+ aml_append(pkg, aml_int(BATTERY_CAPACITY_OF_LOW));
+ /* Battery Capacity Granularity 1 */
+ aml_append(pkg, aml_int(BATTERY_CAPACITY_GRANULARITY));
+ /* Battery Capacity Granularity 2 */
+ aml_append(pkg, aml_int(BATTERY_CAPACITY_GRANULARITY));
+ /* Model Number */
+ aml_append(pkg, aml_string("QBAT001")); /* Model Number */
+ /* Serial Number */
+ aml_append(pkg, aml_string("SN00000")); /* Serial Number */
+ /* Battery Type */
+ aml_append(pkg, aml_string("Virtual")); /* Battery Type */
+ /* OEM Information */
+ aml_append(pkg, aml_string("QEMU")); /* OEM Information */
+ aml_append(method, aml_return(pkg));
+ aml_append(dev, method);
+
+ pkg = aml_package(4);
+ /* Battery State */
+ aml_append(pkg, aml_int(0));
+ /* Battery Present Rate */
+ aml_append(pkg, aml_int(BATTERY_VAL_UNKNOWN));
+ /* Battery Remaining Capacity */
+ aml_append(pkg, aml_int(BATTERY_VAL_UNKNOWN));
+ /* Battery Present Voltage */
+ aml_append(pkg, aml_int(BATTERY_VAL_UNKNOWN));
+ aml_append(dev, aml_name_decl("DBPR", pkg));
+
+ method = aml_method("_BST", 0, AML_NOTSERIALIZED);
+ aml_append(method, aml_store(aml_name("BSTA"), bat_state));
+ aml_append(method, aml_store(aml_name("BRTE"), bat_rate));
+ aml_append(method, aml_store(aml_name("BCRG"), bat_charge));
+ aml_append(method, aml_store(bat_state,
+ aml_index(aml_name("DBPR"), aml_int(0))));
+ aml_append(method, aml_store(bat_rate,
+ aml_index(aml_name("DBPR"), aml_int(1))));
+ aml_append(method, aml_store(bat_charge,
+ aml_index(aml_name("DBPR"), aml_int(2))));
+ aml_append(method, aml_return(aml_name("DBPR")));
+ aml_append(dev, method);
+
+ aml_append(sb_scope, dev);
+ aml_append(scope, sb_scope);
+
+ /* Device Check */
+ method = aml_method("\\_GPE._E07", 0, AML_NOTSERIALIZED);
+ aml_append(method, aml_notify(aml_name("\\_SB.BAT0"), aml_int(0x01)));
+ aml_append(scope, method);
+
+ /* Status Change */
+ method = aml_method("\\_GPE._E08", 0, AML_NOTSERIALIZED);
+ aml_append(method, aml_notify(aml_name("\\_SB.BAT0"), aml_int(0x80)));
+ aml_append(scope, method);
+
+ /* Information Change */
+ method = aml_method("\\_GPE._E09", 0, AML_NOTSERIALIZED);
+ aml_append(method, aml_notify(aml_name("\\_SB.BAT0"), aml_int(0x81)));
+ aml_append(scope, method);
+}
+
+static void battery_class_init(ObjectClass *class, const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(class);
+ AcpiDevAmlIfClass *adevc = ACPI_DEV_AML_IF_CLASS(class);
+
+ dc->realize = battery_realize;
+ device_class_set_props(dc, battery_device_properties);
+ dc->vmsd = &battery_vmstate;
+ adevc->build_dev_aml = build_battery_aml;
+}
+
+static uint64_t battery_ioport_read(void *opaque, hwaddr addr, unsigned size)
+{
+ BatteryState *s = opaque;
+
+ battery_get_dynamic_status(s);
+
+ switch (addr) {
+ case bsta_addr0:
+ return s->state.acc[0];
+ case bsta_addr1:
+ return s->state.acc[1];
+ case bsta_addr2:
+ return s->state.acc[2];
+ case bsta_addr3:
+ return s->state.acc[3];
+ case brte_addr0:
+ return s->rate.acc[0];
+ case brte_addr1:
+ return s->rate.acc[1];
+ case brte_addr2:
+ return s->rate.acc[2];
+ case brte_addr3:
+ return s->rate.acc[3];
+ case bcrg_addr0:
+ return s->charge.acc[0];
+ case bcrg_addr1:
+ return s->charge.acc[1];
+ case bcrg_addr2:
+ return s->charge.acc[2];
+ case bcrg_addr3:
+ return s->charge.acc[3];
+ default:
+ warn_report("Battery: guest read unknown value.");
+ trace_battery_ioport_read_unknown();
+ return 0;
+ }
+}
+
+static const MemoryRegionOps battery_ops = {
+ .read = battery_ioport_read,
+ .impl = {
+ .min_access_size = 1,
+ .max_access_size = 1,
+ },
+};
+
+static void battery_instance_init(Object *obj)
+{
+ BatteryState *s = BATTERY_DEVICE(obj);
+
+ memory_region_init_io(&s->io, obj, &battery_ops, s, "battery",
+ BATTERY_LEN);
+}
+
+static const TypeInfo battery_info = {
+ .name = TYPE_BATTERY,
+ .parent = TYPE_ISA_DEVICE,
+ .instance_size = sizeof(BatteryState),
+ .class_init = battery_class_init,
+ .instance_init = battery_instance_init,
+ .interfaces = (InterfaceInfo[]) {
+ { TYPE_ACPI_DEV_AML_IF },
+ { },
+ },
+};
+
+static BatteryState *find_battery_device(void)
+{
+ Object *o = object_resolve_path_type("", TYPE_BATTERY, NULL);
+ if (!o) {
+ return NULL;
+ }
+ return BATTERY_DEVICE(o);
+}
+
+void qmp_battery_set_state(BatteryInfo *state, Error **errp)
+{
+ BatteryState *s = find_battery_device();
+
+ if (!s) {
+ error_setg(errp, "No battery device found");
+ return;
+ }
+
+ s->qmp_present = state->present;
+ s->qmp_charging = state->charging;
+ s->qmp_discharging = state->discharging;
+ s->qmp_charge_percent = state->charge_percent;
+
+ if (state->has_rate) {
+ s->qmp_rate = state->rate;
+ }
+
+ Object *obj = object_resolve_path_type("", TYPE_ACPI_DEVICE_IF, NULL);
+ if (obj) {
+ acpi_send_event(DEVICE(obj), ACPI_BATTERY_CHANGE_STATUS);
+ }
+}
+
+BatteryInfo *qmp_query_battery(Error **errp)
+{
+ BatteryState *s = find_battery_device();
+ BatteryInfo *ret;
+
+ if (!s) {
+ error_setg(errp, "No battery device found");
+ return NULL;
+ }
+
+ ret = g_new0(BatteryInfo, 1);
+
+ if (s->use_qmp_control) {
+ ret->present = s->qmp_present;
+ ret->charging = s->qmp_charging;
+ ret->discharging = s->qmp_discharging;
+ ret->charge_percent = s->qmp_charge_percent;
+ ret->has_rate = true;
+ ret->rate = s->qmp_rate;
+ } else {
+ battery_get_dynamic_status(s);
+ ret->present = true;
+ ret->charging = !!(s->state.val & BATTERY_CHARGING);
+ ret->discharging = !!(s->state.val & BATTERY_DISCHARGING);
+ ret->charge_percent = (s->charge.val * 100) / BATTERY_FULL_CAP;
+ ret->has_rate = true;
+ ret->rate = s->rate.val;
+ }
+
+ ret->has_remaining_capacity = false;
+ ret->has_design_capacity = true;
+ ret->design_capacity = BATTERY_FULL_CAP;
+
+ return ret;
+}
+
+static void battery_register_types(void)
+{
+ type_register_static(&battery_info);
+}
+
+type_init(battery_register_types)
diff --git a/hw/acpi/meson.build b/hw/acpi/meson.build
index 73f02b9691..10379a7b2c 100644
--- a/hw/acpi/meson.build
+++ b/hw/acpi/meson.build
@@ -31,6 +31,7 @@ acpi_ss.add(when: 'CONFIG_PC', if_false: files('acpi-x86-stub.c'))
if have_tpm
acpi_ss.add(files('tpm.c'))
endif
+acpi_ss.add(when: 'CONFIG_BATTERY', if_true: files('battery.c'))
system_ss.add(when: 'CONFIG_ACPI', if_false: files('acpi-stub.c', 'aml-build-stub.c', 'ghes-stub.c', 'acpi_interface.c'))
system_ss.add(when: 'CONFIG_ACPI_PCI_BRIDGE', if_false: files('pci-bridge-stub.c'))
system_ss.add_all(when: 'CONFIG_ACPI', if_true: acpi_ss)
diff --git a/hw/acpi/trace-events b/hw/acpi/trace-events
index edc93e703c..dd3e815482 100644
--- a/hw/acpi/trace-events
+++ b/hw/acpi/trace-events
@@ -87,3 +87,8 @@ acpi_nvdimm_read_io_port(void) "Alert: we never read _DSM IO Port"
acpi_nvdimm_dsm_mem_addr(uint64_t dsm_mem_addr) "dsm memory address 0x%" PRIx64
acpi_nvdimm_dsm_info(uint32_t revision, uint32_t handle, uint32_t function) "Revision 0x%" PRIx32 " Handle 0x%" PRIx32 " Function 0x%" PRIx32
acpi_nvdimm_invalid_revision(uint32_t revision) "Revision 0x%" PRIx32 " is not supported, expect 0x1"
+
+# 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
+battery_ioport_read_unknown(void) "Battery read unknown"
diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig
index 3a0e2b8ebb..2c878fd112 100644
--- a/hw/i386/Kconfig
+++ b/hw/i386/Kconfig
@@ -39,6 +39,7 @@ config PC
imply VIRTIO_VGA
imply NVDIMM
imply FDC_ISA
+ imply BATTERY
select I8259
select I8254
select PCKBD
diff --git a/hw/i386/acpi-build.c b/hw/i386/acpi-build.c
index 423c4959fe..790b16e582 100644
--- a/hw/i386/acpi-build.c
+++ b/hw/i386/acpi-build.c
@@ -1248,6 +1248,7 @@ build_dsdt(GArray *table_data, BIOSLinker *linker,
aml_append(sb_scope, dev);
}
+
aml_append(dsdt, sb_scope);
if (pm->pcihp_bridge_en || pm->pcihp_root_en) {
diff --git a/include/hw/acpi/acpi_dev_interface.h b/include/hw/acpi/acpi_dev_interface.h
index 68d9d15f50..3064ef6734 100644
--- a/include/hw/acpi/acpi_dev_interface.h
+++ b/include/hw/acpi/acpi_dev_interface.h
@@ -13,6 +13,7 @@ typedef enum {
ACPI_NVDIMM_HOTPLUG_STATUS = 16,
ACPI_VMGENID_CHANGE_STATUS = 32,
ACPI_POWER_DOWN_STATUS = 64,
+ ACPI_BATTERY_CHANGE_STATUS = 128,
} AcpiEventStatusBits;
#define TYPE_ACPI_DEVICE_IF "acpi-device-interface"
diff --git a/include/hw/acpi/battery.h b/include/hw/acpi/battery.h
new file mode 100644
index 0000000000..5c5e83abfa
--- /dev/null
+++ b/include/hw/acpi/battery.h
@@ -0,0 +1,33 @@
+/*
+ * QEMU emulated battery device.
+ *
+ * Copyright (c) 2019 Janus Technologies, Inc. (http://janustech.com)
+ *
+ * Authors:
+ * Leonid Bloch <lb.workbox@gmail.com>
+ * Marcel Apfelbaum <marcel.apfelbaum@gmail.com>
+ * Dmitry Fleytman <dmitry.fleytman@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ */
+
+#ifndef HW_ACPI_BATTERY_H
+#define HW_ACPI_BATTERY_H
+
+#define TYPE_BATTERY "battery"
+#define BATTERY_IOPORT_PROP "ioport"
+#define BATTERY_PATH_PROP "sysfs_path"
+#define BATTERY_PROBE_STATE_INTERVAL "probe_interval"
+
+#define BATTERY_FULL_CAP 10000 /* mWh */
+
+#define BATTERY_CAPACITY_OF_WARNING (BATTERY_FULL_CAP / 10) /* 10% */
+#define BATTERY_CAPACITY_OF_LOW (BATTERY_FULL_CAP / 25) /* 4% */
+#define BATTERY_CAPACITY_GRANULARITY (BATTERY_FULL_CAP / 100) /* 1% */
+
+#define BATTERY_VAL_UNKNOWN 0xFFFFFFFF
+
+#define BATTERY_LEN 0x0C
+
+#endif
diff --git a/qapi/acpi.json b/qapi/acpi.json
index 906b3687a5..d1ad663bfd 100644
--- a/qapi/acpi.json
+++ b/qapi/acpi.json
@@ -142,3 +142,76 @@
##
{ 'event': 'ACPI_DEVICE_OST',
'data': { 'info': 'ACPIOSTInfo' } }
+
+##
+# @BatteryInfo:
+#
+# Battery state information
+#
+# @present: whether the battery is present
+#
+# @charging: whether the battery is charging
+#
+# @discharging: whether the battery is discharging
+#
+# @charge-percent: battery charge percentage (0-100)
+#
+# @rate: charge/discharge rate in mW (optional)
+#
+# @remaining-capacity: remaining capacity in mWh (optional)
+#
+# @design-capacity: design capacity in mWh (optional)
+#
+# Since: 10.2
+##
+{ 'struct': 'BatteryInfo',
+ 'data': { 'present': 'bool',
+ 'charging': 'bool',
+ 'discharging': 'bool',
+ 'charge-percent': 'int',
+ '*rate': 'int',
+ '*remaining-capacity': 'int',
+ '*design-capacity': 'int' } }
+
+##
+# @battery-set-state:
+#
+# Set the state of the emulated battery device
+#
+# @state: new battery state
+#
+
+#
+# Since: 10.2
+#
+# .. qmp-example::
+#
+# -> { "execute": "battery-set-state",
+# "arguments": { "state": { "present": true,
+# "charging": true,
+# "discharging": false,
+# "charge-percent": 85 } } }
+# <- { "return": {} }
+##
+{ 'command': 'battery-set-state',
+ 'data': { 'state': 'BatteryInfo' } }
+
+##
+# @query-battery:
+#
+# Query the current state of the emulated battery device
+#
+# Returns: current battery state
+#
+# Since: 10.2
+#
+# .. qmp-example::
+#
+# -> { "execute": "query-battery" }
+# <- { "return": { "present": true,
+# "charging": true,
+# "discharging": false,
+# "charge-percent": 85 } }
+##
+{ 'command': 'query-battery',
+ 'returns': 'BatteryInfo' }
--
2.50.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v2 3/4] hw/acpi: Introduce the QEMU AC adapter
2025-08-21 17:45 [PATCH v2 0/4] Introduce a battery, AC adapter, and lid button Leonid Bloch
2025-08-21 17:45 ` [PATCH v2 1/4] hw/acpi: Increase the number of possible ACPI interrupts Leonid Bloch
2025-08-21 17:45 ` [PATCH v2 2/4] hw/acpi: Introduce the QEMU Battery Leonid Bloch
@ 2025-08-21 17:45 ` Leonid Bloch
2025-08-21 17:45 ` [PATCH v2 4/4] hw/acpi: Introduce the QEMU lid button Leonid Bloch
2025-08-21 18:03 ` [PATCH v2 0/4] Introduce a battery, AC adapter, and " Leonid Bloch
4 siblings, 0 replies; 7+ messages in thread
From: Leonid Bloch @ 2025-08-21 17:45 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
The AC adapter device communicates AC power state to the guest via ACPI.
It supports two modes of operation:
1. QMP control mode (default): AC adapter state is controlled
programmatically via QMP commands, ensuring deterministic behavior.
2. Host mirroring mode (optional): The device reflects the host's AC
adapter state from sysfs. Probing occurs on guest ACPI requests and
at timed intervals. State changes trigger ACPI notifications.
Properties:
- 'use-qmp': Enable QMP control mode (default: true)
- 'enable-sysfs': Enable host AC adapter mirroring (default: false)
- 'probe_interval': Probe interval in ms for sysfs mode (default: 2000)
- 'sysfs_path': Override default sysfs path /sys/class/power_supply/
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 <lb.workbox@gmail.com>
---
MAINTAINERS | 6 +
docs/specs/acad.rst | 195 ++++++++++++
docs/specs/index.rst | 1 +
hw/acpi/Kconfig | 4 +
hw/acpi/acad.c | 447 +++++++++++++++++++++++++++
hw/acpi/meson.build | 1 +
hw/acpi/trace-events | 5 +
hw/i386/Kconfig | 1 +
include/hw/acpi/acad.h | 27 ++
include/hw/acpi/acpi_dev_interface.h | 1 +
qapi/acpi.json | 49 +++
11 files changed, 737 insertions(+)
create mode 100644 docs/specs/acad.rst
create mode 100644 hw/acpi/acad.c
create mode 100644 include/hw/acpi/acad.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 51af9b7366..612efcb686 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2927,6 +2927,12 @@ S: Maintained
F: hw/acpi/battery.*
F: docs/specs/battery.rst
+AC Adapter
+M: Leonid Bloch <lb.workbox@gmail.com>
+S: Maintained
+F: hw/acpi/acad.*
+F: docs/specs/acad.rst
+
Subsystems
----------
Overall Audio backends
diff --git a/docs/specs/acad.rst b/docs/specs/acad.rst
new file mode 100644
index 0000000000..73d5501b8f
--- /dev/null
+++ b/docs/specs/acad.rst
@@ -0,0 +1,195 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+=================
+AC Adapter Device
+=================
+
+The AC adapter device provides AC power state information to the guest. It
+supports two operating modes:
+
+1. **QMP Control Mode** (default): AC adapter state is controlled via QMP
+ commands, providing deterministic control for testing and migration safety.
+2. **Sysfs Mode**: AC adapter state mirrors the host's physical AC adapter,
+ useful for desktop virtualization where the guest should see the host's
+ power state.
+
+Configuration
+-------------
+
+The AC adapter device is created as an ISA device using ``-device acad``.
+
+Operating Modes
+~~~~~~~~~~~~~~~
+
+**QMP Control Mode** (``use-qmp=true``, default)
+ AC adapter state is controlled via QMP commands. This mode is recommended for:
+
+ * Production environments requiring migration support
+ * Testing with predictable power states
+ * Environments without host AC adapter access
+ * Security-sensitive deployments
+
+**Sysfs Mode** (``enable-sysfs=true``)
+ AC adapter mirrors the host's physical AC adapter. This mode is useful for:
+
+ * Desktop virtualization on laptops
+ * Development and testing with real AC adapter behavior
+
+ Note: Sysfs mode reads host files and runs timers, which may impact
+ security and migration. Use with caution in production.
+
+Properties
+~~~~~~~~~~
+
+``ioport`` (default: 0x53c)
+ I/O port base address for the AC adapter device register.
+
+``use-qmp`` (default: true)
+ Enable QMP control mode. When true, AC adapter state is controlled via
+ QMP commands. Cannot be used together with ``enable-sysfs=true``.
+
+``enable-sysfs`` (default: false)
+ Enable sysfs mode to mirror the host's AC adapter. Cannot be used together
+ with ``use-qmp=true``.
+
+``probe_interval`` (default: 2000)
+ Time interval between periodic probes in milliseconds (sysfs mode only).
+ A zero value disables the periodic probes, and makes the AC adapter state
+ updates occur on guest requests only.
+
+``sysfs_path`` (default: auto-detected)
+ Path to the host's AC adapter sysfs directory (sysfs mode only). By default,
+ the device auto-detects the first AC adapter of type "Mains" in
+ ``/sys/class/power_supply/``. Use this property to specify a different
+ AC adapter, or to provide a custom path for testing purposes.
+
+Host AC Adapter Detection
+-------------------------
+
+The host's AC adapter information is taken from the sysfs AC adapter
+data, located in::
+
+ /sys/class/power_supply/[device of type "Mains"]
+
+The device automatically scans for the first AC adapter with:
+
+- A ``type`` file containing "Mains"
+- An ``online`` file that can be read
+
+If the sysfs path differs, a different AC adapter needs to be probed,
+or even if a "fake" host AC adapter is to be provided, the ``sysfs_path``
+property allows overriding the default detection.
+
+ACPI Interface
+--------------
+
+The AC adapter device is exposed to the guest as an ACPI device with:
+
+- **HID**: ``ACPI0003`` (AC Adapter)
+- **Device Path**: ``\_SB.ADP0``
+- **Notification Values**:
+
+ - ``0x80``: Status change (connected/disconnected)
+
+ACPI Methods
+~~~~~~~~~~~~
+
+``_PSR`` (Power Source)
+ Returns the current AC adapter state (0 = offline, 1 = online).
+
+``_PCL`` (Power Consumer List)
+ Returns the list of devices powered by this adapter.
+
+``_PIF`` (Power Source Information)
+ Returns static information about the power source including model number,
+ serial number, and OEM information.
+
+I/O Interface
+-------------
+
+The device uses a single I/O port register:
+
+- **Port**: ``ioport`` property value (default 0x53c)
+- **Size**: 1 byte
+- **Access**: Read-only
+
+Register Layout
+~~~~~~~~~~~~~~~
+
+**PWRS** (offset 0x00, 1 byte)
+ Current AC adapter state:
+
+ - ``0x00``: AC adapter offline (unplugged)
+ - ``0x01``: AC adapter online (plugged in)
+
+QMP Commands
+------------
+
+When using QMP control mode (default), the following commands are available:
+
+``ac-adapter-set-state``
+ Set the AC adapter connection state.
+
+ * ``connected``: Whether the AC adapter is connected (boolean)
+
+ Example::
+
+ -> { "execute": "ac-adapter-set-state",
+ "arguments": { "connected": true }}
+ <- { "return": {} }
+
+``query-ac-adapter``
+ Query the current AC adapter state.
+
+ Example::
+
+ -> { "execute": "query-ac-adapter" }
+ <- { "return": { "connected": true }}
+
+Examples
+--------
+
+QMP control mode (default - recommended)::
+
+ # Start with QMP control
+ qemu-system-x86_64 -device acad -qmp tcp:localhost:4444,server,wait=off
+
+ # From another terminal, set AC adapter state via QMP:
+ echo '{"execute":"qmp_capabilities"}
+ {"execute":"ac-adapter-set-state",
+ "arguments":{"connected":true}}' | \
+ nc -N localhost 4444
+
+Sysfs mode (mirror host AC adapter)::
+
+ # Enable sysfs mode to mirror host AC adapter
+ qemu-system-x86_64 -device acad,use-qmp=false,enable-sysfs=true
+
+ # Custom probe interval (5 seconds)
+ qemu-system-x86_64 -device acad,use-qmp=false,enable-sysfs=true,probe_interval=5000
+
+ # Specific AC adapter path
+ qemu-system-x86_64 -device acad,use-qmp=false,enable-sysfs=true,sysfs_path=/sys/class/power_supply/ADP1
+
+Testing with fake AC adapter::
+
+ # Create fake AC adapter files for testing
+ mkdir -p /tmp/fake_ac
+ echo "Mains" > /tmp/fake_ac/type
+ echo "1" > /tmp/fake_ac/online # 1 = connected, 0 = disconnected
+
+ # Use fake AC adapter in sysfs mode
+ qemu-system-x86_64 -device acad,use-qmp=false,enable-sysfs=true,sysfs_path=/tmp/fake_ac
+
+ # Update AC adapter state while VM is running (from another terminal)
+ echo "0" > /tmp/fake_ac/online # Disconnect AC adapter
+ echo "1" > /tmp/fake_ac/online # Reconnect AC adapter
+
+Combined with battery device::
+
+ # QMP mode (recommended)
+ qemu-system-x86_64 -device battery -device acad
+
+ # Sysfs mode (desktop virtualization)
+ qemu-system-x86_64 -device battery,use-qmp=false,enable-sysfs=true \
+ -device acad,use-qmp=false,enable-sysfs=true
diff --git a/docs/specs/index.rst b/docs/specs/index.rst
index 616e8228cc..e144afcd90 100644
--- a/docs/specs/index.rst
+++ b/docs/specs/index.rst
@@ -22,6 +22,7 @@ guest hardware that is specific to QEMU.
acpi_pci_hotplug
acpi_nvdimm
acpi_erst
+ acad
battery
sev-guest-firmware
fw_cfg
diff --git a/hw/acpi/Kconfig b/hw/acpi/Kconfig
index 64403378bd..9d28c3addf 100644
--- a/hw/acpi/Kconfig
+++ b/hw/acpi/Kconfig
@@ -69,6 +69,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.c b/hw/acpi/acad.c
new file mode 100644
index 0000000000..699198c194
--- /dev/null
+++ b/hw/acpi/acad.c
@@ -0,0 +1,447 @@
+/*
+ * QEMU emulated AC adapter device.
+ *
+ * Copyright (c) 2019 Janus Technologies, Inc. (http://janustech.com)
+ *
+ * Authors:
+ * Leonid Bloch <lb.workbox@gmail.com>
+ * Marcel Apfelbaum <marcel.apfelbaum@gmail.com>
+ * Dmitry Fleytman <dmitry.fleytman@gmail.com>
+ *
+ * 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 "hw/nvram/fw_cfg.h"
+#include "qapi/error.h"
+#include "qemu/error-report.h"
+#include "hw/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
+
+#define SYSFS_PATH "/sys/class/power_supply"
+#define AC_ADAPTER_TYPE "Mains"
+#define MAX_ALLOWED_TYPE_LENGTH 16
+
+enum {
+ AC_ADAPTER_OFFLINE = 0,
+ AC_ADAPTER_ONLINE = 1,
+};
+
+typedef struct ACADState {
+ ISADevice dev;
+ MemoryRegion io;
+ uint16_t ioport;
+ uint8_t state;
+ bool use_qmp_control;
+ bool qmp_connected;
+ bool enable_sysfs;
+
+ QEMUTimer *probe_state_timer;
+ uint64_t probe_state_interval;
+
+ char *acad_path;
+} ACADState;
+
+static const char *online_file = "online";
+static const char *type_file = "type";
+
+static inline bool acad_file_accessible(char *path, const char *file)
+{
+ char full_path[PATH_MAX];
+ int path_len;
+
+ path_len = snprintf(full_path, PATH_MAX, "%s/%s", path, file);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ return false;
+ }
+
+ if (access(full_path, R_OK) == 0) {
+ return true;
+ }
+ return false;
+}
+
+static void acad_get_state(ACADState *s)
+{
+ char file_path[PATH_MAX];
+ int path_len;
+ uint8_t val;
+ FILE *ff;
+
+ path_len = snprintf(file_path, PATH_MAX, "%s/%s", s->acad_path,
+ online_file);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ warn_report("Could not read the AC adapter state.");
+ return;
+ }
+
+ ff = fopen(file_path, "r");
+ if (ff == NULL) {
+ warn_report("Could not read the AC adapter state.");
+ return;
+ }
+
+ if (!fscanf(ff, "%hhu", &val)) {
+ warn_report("AC adapter state unreadable.");
+ } else {
+ switch (val) {
+ case AC_ADAPTER_OFFLINE:
+ case AC_ADAPTER_ONLINE:
+ s->state = val;
+ break;
+ default:
+ warn_report("AC adapter state undetermined.");
+ }
+ }
+ fclose(ff);
+}
+
+static void acad_get_dynamic_status(ACADState *s)
+{
+ if (s->use_qmp_control) {
+ s->state = s->qmp_connected ? AC_ADAPTER_ONLINE : AC_ADAPTER_OFFLINE;
+ } else if (s->enable_sysfs) {
+ acad_get_state(s);
+ } else {
+ s->state = AC_ADAPTER_OFFLINE;
+ }
+
+ trace_acad_get_dynamic_status(s->state);
+}
+
+static void acad_probe_state(void *opaque)
+{
+ ACADState *s = opaque;
+
+ uint8_t state_before = s->state;
+
+ acad_get_dynamic_status(s);
+
+ if (state_before != s->state) {
+ Object *obj = object_resolve_path_type("", TYPE_ACPI_DEVICE_IF, NULL);
+ acpi_send_event(DEVICE(obj), ACPI_AC_ADAPTER_CHANGE_STATUS);
+ }
+ timer_mod(s->probe_state_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) +
+ s->probe_state_interval);
+}
+
+static void acad_probe_state_timer_init(ACADState *s)
+{
+ if (s->enable_sysfs && s->probe_state_interval > 0) {
+ s->probe_state_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
+ acad_probe_state, s);
+ timer_mod(s->probe_state_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) +
+ s->probe_state_interval);
+ }
+}
+
+static bool acad_verify_sysfs(ACADState *s, char *path)
+{
+ FILE *ff;
+ char type_path[PATH_MAX];
+ int path_len;
+ char val[MAX_ALLOWED_TYPE_LENGTH];
+
+ path_len = snprintf(type_path, PATH_MAX, "%s/%s", path, type_file);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ return false;
+ }
+
+ ff = fopen(type_path, "r");
+ if (ff == NULL) {
+ return false;
+ }
+
+ if (fgets(val, MAX_ALLOWED_TYPE_LENGTH, ff) == NULL) {
+ fclose(ff);
+ return false;
+ } else {
+ val[strcspn(val, "\n")] = 0;
+ if (strncmp(val, AC_ADAPTER_TYPE, MAX_ALLOWED_TYPE_LENGTH)) {
+ fclose(ff);
+ return false;
+ }
+ }
+ fclose(ff);
+
+ return acad_file_accessible(path, online_file);
+}
+
+static bool get_acad_path(DeviceState *dev)
+{
+ ACADState *s = AC_ADAPTER_DEVICE(dev);
+ DIR *dir;
+ struct dirent *ent;
+ char bp[PATH_MAX];
+ int path_len;
+
+ if (s->acad_path) {
+ return acad_verify_sysfs(s, s->acad_path);
+ }
+
+ dir = opendir(SYSFS_PATH);
+ if (dir == NULL) {
+ return false;
+ }
+
+ ent = readdir(dir);
+ while (ent != NULL) {
+ if (ent->d_name[0] != '.') {
+ path_len = snprintf(bp, PATH_MAX, "%s/%s", SYSFS_PATH,
+ ent->d_name);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ return false;
+ }
+ if (acad_verify_sysfs(s, bp)) {
+ qdev_prop_set_string(dev, AC_ADAPTER_PATH_PROP, bp);
+ closedir(dir);
+ return true;
+ }
+ }
+ ent = readdir(dir);
+ }
+ closedir(dir);
+
+ return false;
+}
+
+static void acad_realize(DeviceState *dev, Error **errp)
+{
+ ISADevice *d = ISA_DEVICE(dev);
+ ACADState *s = AC_ADAPTER_DEVICE(dev);
+ FWCfgState *fw_cfg = fw_cfg_find();
+ uint16_t *acad_port;
+ char err_details[32] = {};
+
+ trace_acad_realize();
+
+ if (s->use_qmp_control && s->enable_sysfs) {
+ error_setg(errp, "Cannot enable both QMP control and sysfs mode");
+ return;
+ }
+
+ if (s->enable_sysfs) {
+ if (!s->acad_path) {
+ strcpy(err_details, " Try using 'sysfs_path='");
+ }
+
+ if (!get_acad_path(dev)) {
+ error_setg(errp, "AC adapter sysfs path not found or unreadable.%s",
+ err_details);
+ return;
+ }
+ }
+
+ isa_register_ioport(d, &s->io, s->ioport);
+
+ acad_probe_state_timer_init(s);
+
+ if (!fw_cfg) {
+ return;
+ }
+
+ acad_port = g_malloc(sizeof(*acad_port));
+ *acad_port = cpu_to_le16(s->ioport);
+ fw_cfg_add_file(fw_cfg, "etc/acad-port", acad_port,
+ sizeof(*acad_port));
+}
+
+static const Property acad_device_properties[] = {
+ DEFINE_PROP_UINT16(AC_ADAPTER_IOPORT_PROP, ACADState, ioport, 0x53c),
+ DEFINE_PROP_BOOL("use-qmp", ACADState, use_qmp_control, true),
+ DEFINE_PROP_BOOL("enable-sysfs", ACADState, enable_sysfs, false),
+ DEFINE_PROP_UINT64(AC_ADAPTER_PROBE_STATE_INTERVAL, ACADState,
+ probe_state_interval, 2000),
+ DEFINE_PROP_STRING(AC_ADAPTER_PATH_PROP, ACADState, acad_path),
+};
+
+static const VMStateDescription acad_vmstate = {
+ .name = "acad",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (VMStateField[]) {
+ VMSTATE_UINT16(ioport, ACADState),
+ VMSTATE_UINT64(probe_state_interval, 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._E0A", 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;
+ 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:
+ warn_report("AC adapter: guest read unknown value.");
+ trace_acad_ioport_read_unknown();
+ return 0;
+ }
+}
+
+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(void)
+{
+ Object *o = object_resolve_path_type("", TYPE_AC_ADAPTER, NULL);
+ if (!o) {
+ return NULL;
+ }
+ return AC_ADAPTER_DEVICE(o);
+}
+
+void qmp_ac_adapter_set_state(bool connected, Error **errp)
+{
+ ACADState *s = find_acad_device();
+
+ if (!s) {
+ error_setg(errp, "No AC adapter device found");
+ return;
+ }
+
+ s->qmp_connected = connected;
+
+ Object *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();
+ AcAdapterInfo *ret;
+
+ if (!s) {
+ error_setg(errp, "No AC adapter device found");
+ return NULL;
+ }
+
+ ret = g_new0(AcAdapterInfo, 1);
+
+ if (s->use_qmp_control) {
+ ret->connected = s->qmp_connected;
+ } else {
+ acad_get_dynamic_status(s);
+ ret->connected = (s->state == AC_ADAPTER_ONLINE);
+ }
+
+ 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 10379a7b2c..2b24951f28 100644
--- a/hw/acpi/meson.build
+++ b/hw/acpi/meson.build
@@ -32,6 +32,7 @@ if have_tpm
acpi_ss.add(files('tpm.c'))
endif
acpi_ss.add(when: 'CONFIG_BATTERY', if_true: files('battery.c'))
+acpi_ss.add(when: 'CONFIG_AC_ADAPTER', if_true: files('acad.c'))
system_ss.add(when: 'CONFIG_ACPI', if_false: files('acpi-stub.c', 'aml-build-stub.c', 'ghes-stub.c', 'acpi_interface.c'))
system_ss.add(when: 'CONFIG_ACPI_PCI_BRIDGE', if_false: files('pci-bridge-stub.c'))
system_ss.add_all(when: 'CONFIG_ACPI', if_true: acpi_ss)
diff --git a/hw/acpi/trace-events b/hw/acpi/trace-events
index dd3e815482..68ac6e9701 100644
--- a/hw/acpi/trace-events
+++ b/hw/acpi/trace-events
@@ -92,3 +92,8 @@ acpi_nvdimm_invalid_revision(uint32_t revision) "Revision 0x%" PRIx32 " is not s
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
battery_ioport_read_unknown(void) "Battery read unknown"
+
+# acad.c
+acad_realize(void) "AC adapter device realize entry"
+acad_get_dynamic_status(uint8_t state) "AC adapter read state: %"PRIu8
+acad_ioport_read_unknown(void) "AC adapter read unknown"
diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig
index 2c878fd112..baab382a2e 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..a4e7149488
--- /dev/null
+++ b/include/hw/acpi/acad.h
@@ -0,0 +1,27 @@
+/*
+ * QEMU emulated AC adapter device.
+ *
+ * Copyright (c) 2019 Janus Technologies, Inc. (http://janustech.com)
+ *
+ * Authors:
+ * Leonid Bloch <lb.workbox@gmail.com>
+ * Marcel Apfelbaum <marcel.apfelbaum@gmail.com>
+ * Dmitry Fleytman <dmitry.fleytman@gmail.com>
+ *
+ * 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_PATH_PROP "sysfs_path"
+#define AC_ADAPTER_PROBE_STATE_INTERVAL "probe_interval"
+
+#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 3064ef6734..588fbbd05f 100644
--- a/include/hw/acpi/acpi_dev_interface.h
+++ b/include/hw/acpi/acpi_dev_interface.h
@@ -14,6 +14,7 @@ typedef enum {
ACPI_VMGENID_CHANGE_STATUS = 32,
ACPI_POWER_DOWN_STATUS = 64,
ACPI_BATTERY_CHANGE_STATUS = 128,
+ ACPI_AC_ADAPTER_CHANGE_STATUS = 1024,
} AcpiEventStatusBits;
#define TYPE_ACPI_DEVICE_IF "acpi-device-interface"
diff --git a/qapi/acpi.json b/qapi/acpi.json
index d1ad663bfd..52e151f0e6 100644
--- a/qapi/acpi.json
+++ b/qapi/acpi.json
@@ -215,3 +215,52 @@
##
{ '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: 10.2
+#
+# .. 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: 10.2
+##
+{ 'struct': 'AcAdapterInfo',
+ 'data': { 'connected': 'bool' } }
+
+##
+# @query-ac-adapter:
+#
+# Query the current state of the emulated AC adapter device
+#
+# Returns: AC adapter connection state
+#
+# Since: 10.2
+#
+# .. qmp-example::
+#
+# -> { "execute": "query-ac-adapter" }
+# <- { "return": { "connected": true } }
+##
+{ 'command': 'query-ac-adapter',
+ 'returns': 'AcAdapterInfo' }
--
2.50.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v2 4/4] hw/acpi: Introduce the QEMU lid button
2025-08-21 17:45 [PATCH v2 0/4] Introduce a battery, AC adapter, and lid button Leonid Bloch
` (2 preceding siblings ...)
2025-08-21 17:45 ` [PATCH v2 3/4] hw/acpi: Introduce the QEMU AC adapter Leonid Bloch
@ 2025-08-21 17:45 ` Leonid Bloch
2025-08-21 18:03 ` [PATCH v2 0/4] Introduce a battery, AC adapter, and " Leonid Bloch
4 siblings, 0 replies; 7+ messages in thread
From: Leonid Bloch @ 2025-08-21 17:45 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
The lid button device communicates laptop lid state to the guest via ACPI.
It supports two modes of operation:
1. QMP control mode (default): Lid state is controlled programmatically
via QMP commands for consistent behavior across environments.
2. Host mirroring mode (optional): The device reflects the host's lid
button state from procfs (/proc/acpi/button/lid/*/state). State
changes trigger ACPI notifications to the guest.
Properties:
- 'use-qmp': Enable QMP control mode (default: true)
- 'enable-procfs': Enable host lid button mirroring (default: false)
- 'probe_interval': Probe interval in ms for procfs mode (default: 2000)
- 'procfs_path': Override default procfs path /proc/acpi/button
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 <lb.workbox@gmail.com>
---
MAINTAINERS | 6 +
docs/specs/button.rst | 189 ++++++++++++
docs/specs/index.rst | 1 +
hw/acpi/Kconfig | 4 +
hw/acpi/button.c | 438 +++++++++++++++++++++++++++
hw/acpi/meson.build | 1 +
hw/acpi/trace-events | 5 +
hw/i386/Kconfig | 1 +
include/hw/acpi/acpi_dev_interface.h | 1 +
include/hw/acpi/button.h | 25 ++
qapi/acpi.json | 49 +++
11 files changed, 720 insertions(+)
create mode 100644 docs/specs/button.rst
create mode 100644 hw/acpi/button.c
create mode 100644 include/hw/acpi/button.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 612efcb686..4d54a2ffb5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2933,6 +2933,12 @@ S: Maintained
F: hw/acpi/acad.*
F: docs/specs/acad.rst
+Button
+M: Leonid Bloch <lb.workbox@gmail.com>
+S: Maintained
+F: hw/acpi/button.*
+F: docs/specs/button.rst
+
Subsystems
----------
Overall Audio backends
diff --git a/docs/specs/button.rst b/docs/specs/button.rst
new file mode 100644
index 0000000000..10a940b9f5
--- /dev/null
+++ b/docs/specs/button.rst
@@ -0,0 +1,189 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+========================
+Laptop Lid Button Device
+========================
+
+The button device provides laptop lid button state information to the guest.
+It supports two operating modes:
+
+1. **QMP Control Mode** (default): Lid state is controlled via QMP commands,
+ providing deterministic control for testing and migration safety.
+2. **Procfs Mode**: Lid state mirrors the host's physical lid button, useful
+ for desktop virtualization where the guest should see the host's lid state.
+
+Configuration
+-------------
+
+The lid button device is created as an ISA device using ``-device button``.
+
+Operating Modes
+~~~~~~~~~~~~~~~
+
+**QMP Control Mode** (``use-qmp=true``, default)
+ Lid state is controlled via QMP commands. This mode is recommended for:
+
+ * Production environments requiring migration support
+ * Testing with predictable lid states
+ * Environments without host lid button access
+ * Security-sensitive deployments
+
+**Procfs Mode** (``enable-procfs=true``)
+ Lid mirrors the host's physical lid button. This mode is useful for:
+
+ * Desktop virtualization on laptops
+ * Development and testing with real lid button behavior
+
+ Note: Procfs mode reads host files and runs timers, which may impact
+ security and migration. Use with caution in production.
+
+Properties
+~~~~~~~~~~
+
+``ioport`` (default: 0x53d)
+ I/O port base address for the lid button device register.
+
+``use-qmp`` (default: true)
+ Enable QMP control mode. When true, lid state is controlled via
+ QMP commands. Cannot be used together with ``enable-procfs=true``.
+
+``enable-procfs`` (default: false)
+ Enable procfs mode to mirror the host's lid button. Cannot be used together
+ with ``use-qmp=true``.
+
+``probe_interval`` (default: 2000)
+ Time interval between periodic probes in milliseconds (procfs mode only).
+ The minimum allowed value is 10ms to prevent excessive polling.
+
+``procfs_path`` (default: /proc/acpi/button)
+ Path to the host's lid button procfs directory (procfs mode only). The device
+ will automatically scan this directory to find the lid state file. Use this
+ property to specify a different path or to provide a custom location for
+ testing purposes.
+
+Host Lid Button Detection
+-------------------------
+
+The host's lid button information is taken from::
+
+ /proc/acpi/button/lid/*/state
+
+This file is expected to be formatted as:
+
+- ``state: open`` (if the lid is open)
+- ``state: closed`` (if the lid is closed)
+
+These formats are based on the Linux 'button' driver.
+
+The device automatically scans the ``/proc/acpi/button/lid/`` directory
+for subdirectories containing a readable ``state`` file. If the procfs path
+differs, a different lid button needs to be probed, or even if a "fake" host
+lid button is to be provided, the ``procfs_path`` property allows overriding
+the default detection.
+
+ACPI Interface
+--------------
+
+The lid button device is exposed to the guest as an ACPI device with:
+
+- **HID**: ``PNP0C0D`` (Lid Device)
+- **Device Path**: ``\_SB.LID0``
+- **Notification Values**:
+
+ - ``0x80``: Status change (lid opened/closed)
+
+ACPI Methods
+~~~~~~~~~~~~
+
+``_LID`` (Lid Status)
+ Returns the current lid state (0 = closed, 1 = open).
+
+I/O Interface
+-------------
+
+The device uses a single I/O port register:
+
+- **Port**: ``ioport`` property value (default 0x53d)
+- **Size**: 1 byte
+- **Access**: Read-only
+
+Register Layout
+~~~~~~~~~~~~~~~
+
+**LIDS** (offset 0x00, 1 byte)
+ Current lid state:
+
+ - ``0x00``: Lid closed
+ - ``0x01``: Lid open
+
+QMP Commands
+------------
+
+When using QMP control mode (default), the following commands are available:
+
+``lid-button-set-state``
+ Set the lid button state.
+
+ * ``open``: Whether the lid is open (boolean)
+
+ Example::
+
+ -> { "execute": "lid-button-set-state",
+ "arguments": { "open": true }}
+ <- { "return": {} }
+
+``query-lid-button``
+ Query the current lid button state.
+
+ Example::
+
+ -> { "execute": "query-lid-button" }
+ <- { "return": { "open": true }}
+
+Examples
+--------
+
+QMP control mode (default - recommended)::
+
+ # Start with QMP control
+ qemu-system-x86_64 -device button -qmp tcp:localhost:4444,server,wait=off
+
+ # From another terminal, set lid state via QMP:
+ echo '{"execute":"qmp_capabilities"}
+ {"execute":"lid-button-set-state",
+ "arguments":{"open":false}}' | \
+ nc -N localhost 4444
+
+Procfs mode (mirror host lid button)::
+
+ # Enable procfs mode to mirror host lid button
+ qemu-system-x86_64 -device button,use-qmp=false,enable-procfs=true
+
+ # Custom probe interval (5 seconds)
+ qemu-system-x86_64 -device button,use-qmp=false,enable-procfs=true,probe_interval=5000
+
+ # Custom procfs path
+ qemu-system-x86_64 -device button,use-qmp=false,enable-procfs=true,procfs_path=/custom/path
+
+Testing with fake lid button::
+
+ # Create fake lid button files for testing
+ mkdir -p /tmp/fake_lid/lid/LID0
+ echo "state: open" > /tmp/fake_lid/lid/LID0/state # Format: "state: open" or "state: closed"
+
+ # Use fake lid button in procfs mode
+ qemu-system-x86_64 -device button,use-qmp=false,enable-procfs=true,procfs_path=/tmp/fake_lid
+
+ # Update lid state while VM is running (from another terminal)
+ echo "state: closed" > /tmp/fake_lid/lid/LID0/state # Close lid
+ echo "state: open" > /tmp/fake_lid/lid/LID0/state # Open lid
+
+Combined with other laptop devices::
+
+ # QMP mode (recommended)
+ qemu-system-x86_64 -device battery -device acad -device button
+
+ # Procfs/sysfs mode (desktop virtualization)
+ qemu-system-x86_64 -device battery,use-qmp=false,enable-sysfs=true \
+ -device acad,use-qmp=false,enable-sysfs=true \
+ -device button,use-qmp=false,enable-procfs=true
diff --git a/docs/specs/index.rst b/docs/specs/index.rst
index e144afcd90..e1c9b91b7b 100644
--- a/docs/specs/index.rst
+++ b/docs/specs/index.rst
@@ -24,6 +24,7 @@ guest hardware that is specific to QEMU.
acpi_erst
acad
battery
+ button
sev-guest-firmware
fw_cfg
fsi
diff --git a/hw/acpi/Kconfig b/hw/acpi/Kconfig
index 9d28c3addf..6600685855 100644
--- a/hw/acpi/Kconfig
+++ b/hw/acpi/Kconfig
@@ -73,6 +73,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.c b/hw/acpi/button.c
new file mode 100644
index 0000000000..dfe86af713
--- /dev/null
+++ b/hw/acpi/button.c
@@ -0,0 +1,438 @@
+/*
+ * QEMU emulated lid button device
+ *
+ * Copyright (c) 2019 Janus Technologies, Inc. (http://janustech.com)
+ *
+ * Authors:
+ * Leonid Bloch <lb.workbox@gmail.com>
+ * Marcel Apfelbaum <marcel.apfelbaum@gmail.com>
+ * Dmitry Fleytman <dmitry.fleytman@gmail.com>
+ *
+ * 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 "hw/nvram/fw_cfg.h"
+#include "qapi/error.h"
+#include "qemu/error-report.h"
+#include "hw/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
+
+#define PROCFS_PATH "/proc/acpi/button"
+#define LID_DIR "lid"
+#define LID_STATE_FILE "state"
+#define MIN_BUTTON_PROBE_INTERVAL 10 /* ms */
+#define MAX_ALLOWED_LINE_LENGTH 32 /* For convenience when comparing */
+
+enum {
+ LID_CLOSED = 0,
+ LID_OPEN = 1,
+};
+
+static const char *lid_state[] = { "closed", "open" };
+
+typedef struct BUTTONState {
+ ISADevice dev;
+ MemoryRegion io;
+ uint16_t ioport;
+ uint8_t lid_state;
+ bool use_qmp_control;
+ bool qmp_lid_open;
+ bool enable_procfs;
+
+ QEMUTimer *probe_state_timer;
+ uint64_t probe_state_interval;
+
+ char *button_path;
+ char lid_dir[MAX_ALLOWED_LINE_LENGTH];
+} BUTTONState;
+
+static inline bool button_file_accessible(char *path, const char *dir,
+ char *subdir, const char *file)
+{
+ char full_path[PATH_MAX];
+ int path_len;
+
+ path_len = snprintf(full_path, PATH_MAX, "%s/%s/%s/%s", path, dir, subdir,
+ file);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ return false;
+ }
+
+ if (access(full_path, R_OK) == 0) {
+ return true;
+ }
+ return false;
+}
+
+static void button_get_lid_state(BUTTONState *s)
+{
+ char file_path[PATH_MAX];
+ int path_len;
+ char line[MAX_ALLOWED_LINE_LENGTH];
+ FILE *ff;
+
+ path_len = snprintf(file_path, PATH_MAX, "%s/%s/%s/%s", s->button_path,
+ LID_DIR, s->lid_dir, LID_STATE_FILE);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ warn_report("Could not read the lid state.");
+ return;
+ }
+
+ ff = fopen(file_path, "r");
+ if (ff == NULL) {
+ warn_report("Could not read the lid state.");
+ return;
+ }
+
+ if (fgets(line, MAX_ALLOWED_LINE_LENGTH, ff) == NULL) {
+ warn_report("Lid state unreadable.");
+ } else {
+ if (strstr(line, lid_state[LID_OPEN]) != NULL) {
+ s->lid_state = LID_OPEN;
+ } else if (strstr(line, lid_state[LID_CLOSED]) != NULL) {
+ s->lid_state = LID_CLOSED;
+ } else {
+ warn_report("Lid state undetermined.");
+ }
+ }
+
+ fclose(ff);
+}
+
+static void button_get_dynamic_status(BUTTONState *s)
+{
+ trace_button_get_dynamic_status();
+
+ if (s->use_qmp_control) {
+ s->lid_state = s->qmp_lid_open ? LID_OPEN : LID_CLOSED;
+ } else if (s->enable_procfs) {
+ button_get_lid_state(s);
+ } else {
+ s->lid_state = LID_CLOSED;
+ }
+}
+
+static void button_probe_state(void *opaque)
+{
+ BUTTONState *s = opaque;
+
+ uint8_t lid_state_before = s->lid_state;
+
+ button_get_dynamic_status(s);
+
+ if (lid_state_before != s->lid_state) {
+ Object *obj = object_resolve_path_type("", TYPE_ACPI_DEVICE_IF, NULL);
+ acpi_send_event(DEVICE(obj), ACPI_BUTTON_CHANGE_STATUS);
+ }
+ timer_mod(s->probe_state_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) +
+ s->probe_state_interval);
+}
+
+static void button_probe_state_timer_init(BUTTONState *s)
+{
+ if (s->enable_procfs && s->probe_state_interval > 0) {
+ s->probe_state_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
+ button_probe_state, s);
+ timer_mod(s->probe_state_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) +
+ s->probe_state_interval);
+ }
+}
+
+static inline bool button_verify_lid_procfs(char *path, char *lid_subdir)
+{
+ return button_file_accessible(path, LID_DIR, lid_subdir, LID_STATE_FILE);
+}
+
+static bool button_get_lid_dir(BUTTONState *s, char *path)
+{
+ DIR *dir;
+ char lid_path[PATH_MAX];
+ int path_len;
+ struct dirent *ent;
+
+ path_len = snprintf(lid_path, PATH_MAX, "%s/%s", path, LID_DIR);
+ if (path_len < 0 || path_len >= PATH_MAX) {
+ return false;
+ }
+
+ dir = opendir(lid_path);
+ if (dir == NULL) {
+ return false;
+ }
+
+ ent = readdir(dir);
+ while (ent != NULL) {
+ if (ent->d_name[0] != '.') {
+ if (button_verify_lid_procfs(path, ent->d_name)) {
+ path_len = snprintf(s->lid_dir, strlen(ent->d_name) + 1, "%s",
+ ent->d_name);
+ if (path_len < 0 || path_len > strlen(ent->d_name)) {
+ return false;
+ }
+ closedir(dir);
+ return true;
+ }
+ }
+ ent = readdir(dir);
+ }
+ closedir(dir);
+ return false;
+}
+
+static bool get_button_path(DeviceState *dev)
+{
+ BUTTONState *s = BUTTON_DEVICE(dev);
+ char procfs_path[PATH_MAX];
+ int path_len;
+
+ if (s->button_path) {
+ path_len = snprintf(procfs_path, strlen(s->button_path) + 1, "%s",
+ s->button_path);
+ if (path_len < 0 || path_len > strlen(s->button_path)) {
+ return false;
+ }
+ } else {
+ path_len = snprintf(procfs_path, sizeof(PROCFS_PATH), "%s",
+ PROCFS_PATH);
+ if (path_len < 0 || path_len >= sizeof(PROCFS_PATH)) {
+ return false;
+ }
+ }
+
+ if (button_get_lid_dir(s, procfs_path)) {
+ qdev_prop_set_string(dev, BUTTON_PATH_PROP, procfs_path);
+ return true;
+ }
+
+ return false;
+}
+
+static void button_realize(DeviceState *dev, Error **errp)
+{
+ ISADevice *d = ISA_DEVICE(dev);
+ BUTTONState *s = BUTTON_DEVICE(dev);
+ FWCfgState *fw_cfg = fw_cfg_find();
+ uint16_t *button_port;
+ char err_details[32] = {};
+
+ trace_button_realize();
+
+ if (s->use_qmp_control && s->enable_procfs) {
+ error_setg(errp, "Cannot enable both QMP control and procfs mode");
+ return;
+ }
+
+ /* Initialize lid to open by default when in QMP mode */
+ if (s->use_qmp_control) {
+ s->qmp_lid_open = true;
+ }
+
+ if (s->probe_state_interval < MIN_BUTTON_PROBE_INTERVAL) {
+ error_setg(errp, "'probe_state_interval' must be greater than %d ms",
+ MIN_BUTTON_PROBE_INTERVAL);
+ return;
+ }
+
+ if (s->enable_procfs) {
+ if (!s->button_path) {
+ strcpy(err_details, " Try using 'procfs_path='");
+ }
+
+ if (!get_button_path(dev)) {
+ error_setg(errp, "Button procfs path not found or unreadable.%s",
+ err_details);
+ return;
+ }
+ }
+
+ isa_register_ioport(d, &s->io, s->ioport);
+
+ button_probe_state_timer_init(s);
+
+ if (!fw_cfg) {
+ return;
+ }
+
+ button_port = g_malloc(sizeof(*button_port));
+ *button_port = cpu_to_le16(s->ioport);
+ fw_cfg_add_file(fw_cfg, "etc/button-port", button_port,
+ sizeof(*button_port));
+}
+
+static const Property button_device_properties[] = {
+ DEFINE_PROP_UINT16(BUTTON_IOPORT_PROP, BUTTONState, ioport, 0x53d),
+ DEFINE_PROP_BOOL("use-qmp", BUTTONState, use_qmp_control, true),
+ DEFINE_PROP_BOOL("enable-procfs", BUTTONState, enable_procfs, false),
+ DEFINE_PROP_UINT64(BUTTON_PROBE_STATE_INTERVAL, BUTTONState,
+ probe_state_interval, 2000),
+ DEFINE_PROP_STRING(BUTTON_PATH_PROP, BUTTONState, button_path),
+};
+
+static const VMStateDescription button_vmstate = {
+ .name = "button",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (VMStateField[]) {
+ VMSTATE_UINT16(ioport, BUTTONState),
+ VMSTATE_UINT64(probe_state_interval, 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._E0B", 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;
+ 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:
+ warn_report("Button: guest read unknown value.");
+ trace_button_ioport_read_unknown();
+ return 0;
+ }
+}
+
+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(void)
+{
+ Object *o = object_resolve_path_type("", TYPE_BUTTON, NULL);
+ if (!o) {
+ return NULL;
+ }
+ return BUTTON_DEVICE(o);
+}
+
+void qmp_lid_button_set_state(bool open, Error **errp)
+{
+ BUTTONState *s = find_button_device();
+
+ if (!s) {
+ error_setg(errp, "No lid button device found");
+ return;
+ }
+
+ s->qmp_lid_open = open;
+
+ Object *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();
+ LidButtonInfo *ret;
+
+ if (!s) {
+ error_setg(errp, "No lid button device found");
+ return NULL;
+ }
+
+ ret = g_new0(LidButtonInfo, 1);
+
+ if (s->use_qmp_control) {
+ ret->open = s->qmp_lid_open;
+ } else {
+ button_get_dynamic_status(s);
+ ret->open = (s->lid_state == 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 2b24951f28..94993ecb9a 100644
--- a/hw/acpi/meson.build
+++ b/hw/acpi/meson.build
@@ -33,6 +33,7 @@ if have_tpm
endif
acpi_ss.add(when: 'CONFIG_BATTERY', if_true: files('battery.c'))
acpi_ss.add(when: 'CONFIG_AC_ADAPTER', if_true: files('acad.c'))
+acpi_ss.add(when: 'CONFIG_BUTTON', if_true: files('button.c'))
system_ss.add(when: 'CONFIG_ACPI', if_false: files('acpi-stub.c', 'aml-build-stub.c', 'ghes-stub.c', 'acpi_interface.c'))
system_ss.add(when: 'CONFIG_ACPI_PCI_BRIDGE', if_false: files('pci-bridge-stub.c'))
system_ss.add_all(when: 'CONFIG_ACPI', if_true: acpi_ss)
diff --git a/hw/acpi/trace-events b/hw/acpi/trace-events
index 68ac6e9701..816f027bb3 100644
--- a/hw/acpi/trace-events
+++ b/hw/acpi/trace-events
@@ -97,3 +97,8 @@ battery_ioport_read_unknown(void) "Battery read unknown"
acad_realize(void) "AC adapter device realize entry"
acad_get_dynamic_status(uint8_t state) "AC adapter read state: %"PRIu8
acad_ioport_read_unknown(void) "AC adapter read unknown"
+
+# button.c
+button_realize(void) "Button device realize entry"
+button_get_dynamic_status(void) "Button read dynamic status entry"
+button_ioport_read_unknown(void) "Button read unknown"
diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig
index baab382a2e..1d2d809028 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 588fbbd05f..4f5bcc15da 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_BATTERY_CHANGE_STATUS = 128,
ACPI_AC_ADAPTER_CHANGE_STATUS = 1024,
+ ACPI_BUTTON_CHANGE_STATUS = 2048,
} 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..fa5f7001b2
--- /dev/null
+++ b/include/hw/acpi/button.h
@@ -0,0 +1,25 @@
+/*
+ * QEMU emulated button device.
+ *
+ * Copyright (c) 2019 Janus Technologies, Inc. (http://janustech.com)
+ *
+ * Authors:
+ * Leonid Bloch <lb.workbox@gmail.com>
+ * Marcel Apfelbaum <marcel.apfelbaum@gmail.com>
+ * Dmitry Fleytman <dmitry.fleytman@gmail.com>
+ *
+ * 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_PATH_PROP "procfs_path"
+#define BUTTON_PROBE_STATE_INTERVAL "probe_interval"
+
+#define BUTTON_LEN 1
+
+#endif
diff --git a/qapi/acpi.json b/qapi/acpi.json
index 52e151f0e6..bcbddbfbd5 100644
--- a/qapi/acpi.json
+++ b/qapi/acpi.json
@@ -264,3 +264,52 @@
##
{ '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: 10.2
+#
+# .. 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: 10.2
+##
+{ 'struct': 'LidButtonInfo',
+ 'data': { 'open': 'bool' } }
+
+##
+# @query-lid-button:
+#
+# Query the current state of the emulated laptop lid button device
+#
+# Returns: lid button state
+#
+# Since: 10.2
+#
+# .. qmp-example::
+#
+# -> { "execute": "query-lid-button" }
+# <- { "return": { "open": true } }
+##
+{ 'command': 'query-lid-button',
+ 'returns': 'LidButtonInfo' }
--
2.50.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH v2 0/4] Introduce a battery, AC adapter, and lid button
2025-08-21 17:45 [PATCH v2 0/4] Introduce a battery, AC adapter, and lid button Leonid Bloch
` (3 preceding siblings ...)
2025-08-21 17:45 ` [PATCH v2 4/4] hw/acpi: Introduce the QEMU lid button Leonid Bloch
@ 2025-08-21 18:03 ` Leonid Bloch
4 siblings, 0 replies; 7+ messages in thread
From: Leonid Bloch @ 2025-08-21 18:03 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: qemu-devel
[-- Attachment #1: Type: text/plain, Size: 4404 bytes --]
GitHub PR for ease of review: https://github.com/blochl/qemu/pull/2
Link to v1:
https://lists.gnu.org/archive/html/qemu-devel/2021-01/msg05246.html
On Thu, Aug 21, 2025 at 8:46 PM Leonid Bloch <lb.workbox@gmail.com> wrote:
> First, I must apologize for the extremely long delay since v1 - it has been
> nearly 4 years, which is unacceptable. Life circumstances intervened.
>
> This series introduces three ACPI devices that are particularly useful
> for laptop/mobile virtualization:
>
> * Battery
> * AC adapter
> * Laptop lid button
>
> Changes in v2:
> --------------
> Based on the feedback from Philippe Mathieu-Daudé and Michael S. Tsirkin:
>
> * Complete redesign with dual-mode operation:
> - QMP control mode (default): Devices are controlled via QMP commands,
> providing deterministic behavior essential for migration and CI/testing
> - Host mirroring mode (opt-in): Original sysfs/procfs monitoring
> behavior,
> now disabled by default
>
> * Migrated to modern QEMU ACPI architecture:
> - Devices now implement ACPI_DEV_AML_IF interface
> - AML generation moved from centralized acpi-build.c to device files
>
> * Added a QMP interface:
> - battery-set-state/query-battery
> - ac-adapter-set-state/query-ac-adapter
> - lid-button-set-state/query-lid-button
>
> * Documentation improvements:
> - Converted to .rst format
> - Added examples for both QMP and "fake" sysfs/procfs testing
>
> The dual-mode design ensures these devices are migration-safe and
> deterministic by default, while still allowing host state mirroring
> when explicitly requested for desktop use cases.
>
> Use cases:
> ----------
> 1. Testing: CI systems can programmatically control power states
> 2. Cloud: Expose virtual battery for usage-based resource limiting
> 3. Desktop virtualization: Mirror host laptop state to guest (opt-in)
> 4. Development: Test power management without physical hardware
>
> Example usage:
> --------------
> # Default QMP-controlled battery
> qemu-system-x86_64 -device battery
>
> # Mirror host battery
> qemu-system-x86_64 -device battery,use-qmp=false,enable-sysfs=true
>
> # Control via QMP
> {"execute": "battery-set-state",
> "arguments": {"state": {"present": true, "charging": false,
> "discharging": true, "charge-percent": 42,
> "rate": 500}}}
>
> The series has been tested with Windows and Linux guests, correctly
> showing battery status, AC adapter state, and lid button events in
> guest UIs and triggering appropriate power management actions.
>
> Thanks again for your patience and feedback.
> Leonid.
>
> Leonid Bloch (4):
> hw/acpi: Increase the number of possible ACPI interrupts
> hw/acpi: Introduce the QEMU Battery
> hw/acpi: Introduce the QEMU AC adapter
> hw/acpi: Introduce the QEMU lid button
>
> MAINTAINERS | 18 +
> docs/specs/acad.rst | 195 +++++++
> docs/specs/battery.rst | 225 ++++++++
> docs/specs/button.rst | 189 +++++++
> docs/specs/index.rst | 3 +
> hw/acpi/Kconfig | 12 +
> hw/acpi/acad.c | 447 ++++++++++++++++
> hw/acpi/battery.c | 735 +++++++++++++++++++++++++++
> hw/acpi/button.c | 438 ++++++++++++++++
> hw/acpi/core.c | 17 +-
> hw/acpi/meson.build | 3 +
> hw/acpi/trace-events | 15 +
> hw/i386/Kconfig | 3 +
> hw/i386/acpi-build.c | 1 +
> include/hw/acpi/acad.h | 27 +
> include/hw/acpi/acpi_dev_interface.h | 3 +
> include/hw/acpi/battery.h | 33 ++
> include/hw/acpi/button.h | 25 +
> qapi/acpi.json | 171 +++++++
> 19 files changed, 2558 insertions(+), 2 deletions(-)
> create mode 100644 docs/specs/acad.rst
> create mode 100644 docs/specs/battery.rst
> create mode 100644 docs/specs/button.rst
> create mode 100644 hw/acpi/acad.c
> create mode 100644 hw/acpi/battery.c
> create mode 100644 hw/acpi/button.c
> create mode 100644 include/hw/acpi/acad.h
> create mode 100644 include/hw/acpi/battery.h
> create mode 100644 include/hw/acpi/button.h
>
> --
> 2.50.1
>
>
[-- Attachment #2: Type: text/html, Size: 5433 bytes --]
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v2 1/4] hw/acpi: Increase the number of possible ACPI interrupts
2025-08-21 17:45 ` [PATCH v2 1/4] hw/acpi: Increase the number of possible ACPI interrupts Leonid Bloch
@ 2025-08-26 12:47 ` Igor Mammedov
0 siblings, 0 replies; 7+ messages in thread
From: Igor Mammedov @ 2025-08-26 12:47 UTC (permalink / raw)
To: Leonid Bloch
Cc: Michael S . Tsirkin, Ani Sinha, Paolo Bonzini, Richard Henderson,
Eduardo Habkost, Eric Blake, Markus Armbruster, Marcel Apfelbaum,
Dmitry Fleytman, qemu-devel
On Thu, 21 Aug 2025 20:45:49 +0300
Leonid Bloch <lb.workbox@gmail.com> wrote:
> Increase the number of possible ACPI interrupts from 8, to the maximum
> available: 64 by default.
for piix4 we have 'GPE_LEN 4', which gives us 2 bitmaps (STS/EN) 16bit each.
For ICH9_PMIO_GPE0_LEN 16 => 8bytes/bitmap
so numbers above would vary on used machine type
also commit message should mention why this is needed.
>
> Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
> ---
> hw/acpi/core.c | 17 +++++++++++++++--
> 1 file changed, 15 insertions(+), 2 deletions(-)
>
> diff --git a/hw/acpi/core.c b/hw/acpi/core.c
> index 58f8964e13..447ff07878 100644
> --- a/hw/acpi/core.c
> +++ b/hw/acpi/core.c
> @@ -729,19 +729,32 @@ uint32_t acpi_gpe_ioport_readb(ACPIREGS *ar, uint32_t addr)
> void acpi_send_gpe_event(ACPIREGS *ar, qemu_irq irq,
> AcpiEventStatusBits status)
> {
> - ar->gpe.sts[0] |= status;
> + int i;
> +
> + AcpiEventStatusBits st = status;
newline here and remove newline above
> + for (i = 0; i < ar->gpe.len / 2; i++) {
> + ar->gpe.sts[i] |= st;
> + st >>= (sizeof(ar->gpe.sts[0]) * CHAR_BIT);
perhaps use TYPE_WIDTH()
> + }
>
> acpi_update_sci(ar, irq);
> }
>
> void acpi_update_sci(ACPIREGS *regs, qemu_irq irq)
> {
> int sci_level, pm1a_sts;
> + uint64_t gpe_sci = 0;
> + int i;
>
> pm1a_sts = acpi_pm1_evt_get_sts(regs);
>
> + for (i = 0; i < regs->gpe.len / 2; i++) {
> + gpe_sci |= (regs->gpe.sts[i] & regs->gpe.en[i]);
^^^
make it bool and then !!(regs->gpe.sts[i] & regs->gpe.en[i])
> + }
or maybe instead of opencoding bitmaps, use bitmap API from bitops.h
> sci_level = ((pm1a_sts &
> regs->pm1.evt.en & ACPI_BITMASK_PM1_COMMON_ENABLED) != 0) ||
> - ((regs->gpe.sts[0] & regs->gpe.en[0]) != 0);
> + (gpe_sci != 0);
>
> qemu_set_irq(irq, sci_level);
>
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2025-08-26 12:49 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-08-21 17:45 [PATCH v2 0/4] Introduce a battery, AC adapter, and lid button Leonid Bloch
2025-08-21 17:45 ` [PATCH v2 1/4] hw/acpi: Increase the number of possible ACPI interrupts Leonid Bloch
2025-08-26 12:47 ` Igor Mammedov
2025-08-21 17:45 ` [PATCH v2 2/4] hw/acpi: Introduce the QEMU Battery Leonid Bloch
2025-08-21 17:45 ` [PATCH v2 3/4] hw/acpi: Introduce the QEMU AC adapter Leonid Bloch
2025-08-21 17:45 ` [PATCH v2 4/4] hw/acpi: Introduce the QEMU lid button Leonid Bloch
2025-08-21 18:03 ` [PATCH v2 0/4] Introduce a battery, AC adapter, and " Leonid Bloch
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).