The battery device communicates battery state to the guest via ACPI. Battery state is controlled programmatically via QMP commands, making the device deterministic and migration-safe.
Properties: - 'ioport': I/O port base address (default: 0x530) 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 provides a stable interface for virtualization management systems. Signed-off-by: Leonid Bloch <[email protected]> Signed-off-by: Marcel Apfelbaum <[email protected]> --- MAINTAINERS | 2 + hw/acpi/Kconfig | 4 + hw/acpi/battery-stub.c | 20 ++ hw/acpi/battery.c | 364 +++++++++++++++++++++++++++ hw/acpi/meson.build | 2 + hw/acpi/trace-events | 4 + hw/i386/Kconfig | 1 + include/hw/acpi/acpi_dev_interface.h | 1 + include/hw/acpi/battery.h | 32 +++ qapi/acpi.json | 71 ++++++ 10 files changed, 501 insertions(+) create mode 100644 hw/acpi/battery-stub.c create mode 100644 hw/acpi/battery.c create mode 100644 include/hw/acpi/battery.h diff --git a/MAINTAINERS b/MAINTAINERS index e356f46a58..ce50329f48 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -3040,6 +3040,8 @@ Battery M: Leonid Bloch <[email protected]> S: Maintained F: docs/specs/battery.rst +F: hw/acpi/battery* +F: include/hw/acpi/battery.h Subsystems ---------- diff --git a/hw/acpi/Kconfig b/hw/acpi/Kconfig index daabbe6cd1..6b2c46d37a 100644 --- a/hw/acpi/Kconfig +++ b/hw/acpi/Kconfig @@ -88,3 +88,7 @@ config ACPI_ERST config ACPI_CXL bool depends on ACPI + +config BATTERY + bool + depends on ACPI diff --git a/hw/acpi/battery-stub.c b/hw/acpi/battery-stub.c new file mode 100644 index 0000000000..d2f13b51c1 --- /dev/null +++ b/hw/acpi/battery-stub.c @@ -0,0 +1,20 @@ +/* + * QEMU emulated battery device - QMP stubs. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "qapi/qapi-commands-acpi.h" + +void qmp_battery_set_state(BatteryInfo *state, Error **errp) +{ + error_setg(errp, "No battery device found"); +} + +BatteryInfo *qmp_query_battery(Error **errp) +{ + error_setg(errp, "No battery device found"); + return NULL; +} diff --git a/hw/acpi/battery.c b/hw/acpi/battery.c new file mode 100644 index 0000000000..35b81ad486 --- /dev/null +++ b/hw/acpi/battery.c @@ -0,0 +1,364 @@ +/* + * QEMU emulated battery device. + * + * Copyright (c) 2019-2026 Janus Technologies, Inc. (http://janustech.com) + * + * Authors: + * Leonid Bloch <[email protected]> + * Marcel Apfelbaum <[email protected]> + * Dmitry Fleytman <[email protected]> + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + */ + +#include "qemu/osdep.h" +#include "trace.h" +#include "hw/isa/isa.h" +#include "hw/acpi/acpi.h" +#include "qapi/error.h" +#include "hw/core/qdev-properties.h" +#include "migration/vmstate.h" +#include "hw/acpi/acpi_aml_interface.h" +#include "qapi/qapi-commands-acpi.h" + +#include "hw/acpi/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 BATTERY_PRESENT 0x10 /* ACPI _STA bit 4 */ + +typedef struct BatteryState { + ISADevice dev; + MemoryRegion io; + uint16_t ioport; + uint32_t state; + uint32_t rate; + uint32_t charge; + bool qmp_present; + bool qmp_charging; + bool qmp_discharging; + int32_t qmp_charge_percent; + int32_t qmp_rate; +} BatteryState; + +enum { + BSTA_ADDR = 0, + BRTE_ADDR = 4, + BCRG_ADDR = 8, +}; + +static void battery_get_dynamic_status(BatteryState *s) +{ + s->state = 0; + if (s->qmp_present) { + s->state |= BATTERY_PRESENT; + if (s->qmp_charging) { + s->state |= BATTERY_CHARGING; + } + if (s->qmp_discharging) { + s->state |= BATTERY_DISCHARGING; + } + } + s->rate = s->qmp_rate; + s->charge = (s->qmp_charge_percent * BATTERY_FULL_CAP) / 100; + + trace_battery_get_dynamic_status(s->state, s->rate, s->charge); +} + +static void battery_realize(DeviceState *dev, Error **errp) +{ + ISADevice *d = ISA_DEVICE(dev); + BatteryState *s = BATTERY_DEVICE(dev); + bool ambiguous; + + trace_battery_realize(); + + object_resolve_path_type("", TYPE_BATTERY, &ambiguous); + if (ambiguous) { + error_setg(errp, "at most one %s device is permitted", TYPE_BATTERY); + return; + } + + /* Initialize QMP state to sensible defaults */ + 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 */ + + isa_register_ioport(d, &s->io, s->ioport); +} + +static const Property battery_device_properties[] = { + DEFINE_PROP_UINT16(BATTERY_IOPORT_PROP, BatteryState, ioport, 0x530), +}; + +static const VMStateDescription battery_vmstate = { + .name = "battery", + .version_id = 1, + .minimum_version_id = 1, + .fields = (VMStateField[]) { + VMSTATE_BOOL(qmp_present, BatteryState), + VMSTATE_BOOL(qmp_charging, BatteryState), + VMSTATE_BOOL(qmp_discharging, BatteryState), + VMSTATE_INT32(qmp_charge_percent, BatteryState), + VMSTATE_INT32(qmp_rate, 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"))); + + 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("_STA", 0, AML_NOTSERIALIZED); + aml_append(method, aml_return(aml_or(aml_int(0x0F), + aml_and(aml_name("BSTA"), + aml_int(0x10), NULL), + NULL))); + aml_append(dev, method); + + 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_DESIGN_VOLTAGE)); + /* 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_DESIGN_VOLTAGE)); + aml_append(dev, aml_name_decl("DBPR", pkg)); + + method = aml_method("_BST", 0, AML_NOTSERIALIZED); + aml_append(method, aml_store(aml_and(aml_name("BSTA"), aml_int(0x0F), + NULL), + 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); + + /* 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); +} + +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; + dc->hotpluggable = false; + 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_ADDR: + return s->state; + case BRTE_ADDR: + return s->rate; + case BCRG_ADDR: + return s->charge; + default: + g_assert_not_reached(); + } +} + +static const MemoryRegionOps battery_ops = { + .read = battery_ioport_read, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid = { + .min_access_size = 4, + .max_access_size = 4, + }, + .impl = { + .min_access_size = 4, + .max_access_size = 4, + }, +}; + +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(Error **errp) +{ + bool ambiguous; + Object *o = object_resolve_path_type("", TYPE_BATTERY, &ambiguous); + + if (!o) { + error_setg(errp, "No battery device found"); + return NULL; + } + if (ambiguous) { + error_setg(errp, "More than one battery device present"); + return NULL; + } + return BATTERY_DEVICE(o); +} + +void qmp_battery_set_state(BatteryInfo *state, Error **errp) +{ + BatteryState *s = find_battery_device(errp); + Object *obj; + + if (!s) { + return; + } + + if (state->charging && state->discharging) { + error_setg(errp, + "'charging' and 'discharging' are mutually exclusive"); + return; + } + if (!state->present && (state->charging || state->discharging)) { + error_setg(errp, + "'charging'/'discharging' require 'present' to be true"); + return; + } + if (state->charge_percent < 0 || state->charge_percent > 100) { + error_setg(errp, "'charge-percent' must be in the range 0..100"); + return; + } + if (state->has_rate && (state->rate < 0 || state->rate > INT32_MAX)) { + error_setg(errp, "'rate' must be in the range 0..0x%" PRIX32, + (uint32_t)INT32_MAX); + 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; + } + + 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(errp); + BatteryInfo *ret; + + if (!s) { + return NULL; + } + + ret = g_new0(BatteryInfo, 1); + + 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; + + 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 1c5251909b..e6bc78274e 100644 --- a/hw/acpi/meson.build +++ b/hw/acpi/meson.build @@ -35,6 +35,8 @@ if have_tpm endif stub_ss.add(files('acpi-stub.c', 'aml-build-stub.c', 'ghes-stub.c')) stub_ss.add(files('pci-bridge-stub.c')) +acpi_ss.add(when: 'CONFIG_BATTERY', if_true: files('battery.c')) +stub_ss.add(files('battery-stub.c')) system_ss.add_all(when: 'CONFIG_ACPI', if_true: acpi_ss) system_ss.add(when: 'CONFIG_GHES_CPER', if_true: files('ghes_cper.c')) stub_ss.add(files('ghes_cper_stub.c')) diff --git a/hw/acpi/trace-events b/hw/acpi/trace-events index edc93e703c..8a6ab91a13 100644 --- a/hw/acpi/trace-events +++ b/hw/acpi/trace-events @@ -87,3 +87,7 @@ 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 diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig index 12473acaa7..94004ffeb2 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/include/hw/acpi/acpi_dev_interface.h b/include/hw/acpi/acpi_dev_interface.h index 65debb90a8..a6f9022c0b 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_GENERIC_ERROR = 128, + ACPI_BATTERY_CHANGE_STATUS = 256, } 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..eaff760db9 --- /dev/null +++ b/include/hw/acpi/battery.h @@ -0,0 +1,32 @@ +/* + * QEMU emulated battery device. + * + * Copyright (c) 2019-2026 Janus Technologies, Inc. (http://janustech.com) + * + * Authors: + * Leonid Bloch <[email protected]> + * Marcel Apfelbaum <[email protected]> + * Dmitry Fleytman <[email protected]> + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + */ + +#ifndef HW_ACPI_BATTERY_H +#define HW_ACPI_BATTERY_H + +#define TYPE_BATTERY "battery" +#define BATTERY_IOPORT_PROP "ioport" + +#define BATTERY_FULL_CAP 10000 /* mWh */ +#define BATTERY_DESIGN_VOLTAGE 12000 /* mV */ + +#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..4711a05614 100644 --- a/qapi/acpi.json +++ b/qapi/acpi.json @@ -142,3 +142,74 @@ ## { '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: 11.1 +## +{ '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: 11.1 +# +# .. 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: 11.1 +# +# .. qmp-example:: +# +# -> { "execute": "query-battery" } +# <- { "return": { "present": true, +# "charging": true, +# "discharging": false, +# "charge-percent": 85 } } +## +{ 'command': 'query-battery', + 'returns': 'BatteryInfo' } -- 2.54.0
