From nobody Sun Sep 28 15:29:10 2025 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=none dis=none) header.from=gmail.com ARC-Seal: i=1; a=rsa-sha256; t=1756332181; cv=none; d=zohomail.com; s=zohoarc; b=VejwO46/srpTXXgfWWqSTcyhtzsVHGi77ZRIbkssShNp4/vFvtxwp8tLWh029sLILuRSjpFFzXKIwGcP/UvptETtgYVuWHFhs9tGCeh8rFqifDjinmDsRnbxJbrmVUZlJMUV00r+2E7zmohYZzo12gWJ0SXe8IWRWajvj1ttAJY= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1756332181; h=Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=BYDCRX59FanNn5HNp7EQuX7HSdi+9rNxfs8qelh7zGc=; b=mJ9STFfxVZ6axHC31QtPO5M5IDHwGC07q2wXo//+E9ZICyxYbh1fWTlxheFsg/mtfQRdJZ8AlGAoDNxGtlvyoFyB6l+/VZF2qyy0/fIfHkQbeTVaEp804TMEB/sUfOII7xZzaaB9h8xBqTEHYVKYg0Lcq0gSjq3MYpW1B2ewcno= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1756332181154638.9019452984044; Wed, 27 Aug 2025 15:03:01 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1urOD2-0006fz-FH; Wed, 27 Aug 2025 18:01:36 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1urOCz-0006f2-Hx for qemu-devel@nongnu.org; Wed, 27 Aug 2025 18:01:33 -0400 Received: from mail-wm1-x32a.google.com ([2a00:1450:4864:20::32a]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1urOCw-0007eB-Sp for qemu-devel@nongnu.org; Wed, 27 Aug 2025 18:01:33 -0400 Received: by mail-wm1-x32a.google.com with SMTP id 5b1f17b1804b1-45b4d8921f2so2339595e9.2 for ; Wed, 27 Aug 2025 15:01:30 -0700 (PDT) Received: from localhost.localdomain (46-116-237-160.bb.netvision.net.il. [46.116.237.160]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-45b797ce2b8sm4116755e9.12.2025.08.27.15.01.27 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 27 Aug 2025 15:01:28 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1756332089; x=1756936889; darn=nongnu.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=BYDCRX59FanNn5HNp7EQuX7HSdi+9rNxfs8qelh7zGc=; b=RClTksj5Di1aFYsIBSdFQxSBTAa5vLJ6GTJY7+d7BdcHJfEJXV90TAmrzA/OBIVvm0 OTzYP/zd26fvJjhI3Z61fHjIsPuhqT3DJ9w9COD6ADRN/j7M51/g39pQTFCJUw8cU6ai GYBU/B+cHh4h2HEhxDCcCAmlPrfHhazGWMNojVSmT8H2oDgn4Xdh0+aQWVKrmdJK0YA8 /1IcgvNybau6aCJp+5hrY21z1lyThW9sMQ8uB+jTQGZRWDpJ0W8K56C2YuC8eMVq98Gd dtQkcvVGeo54HAtvj9gSEusmaAaR5mG9+zmws5cHeEc7RTZU1tRIDJ7eBxLo1aZYcXUv tysg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1756332089; x=1756936889; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=BYDCRX59FanNn5HNp7EQuX7HSdi+9rNxfs8qelh7zGc=; b=GxHUJeyeD5Xb3DNodth4qccHEIxSKKddNWhE8bREyiLxpm1i/MQSZI0k4W8SUOk3u7 83qBZrUbrrjIRis1Y51OTQbGZt9Do6KfGj159/a8NjxU6S8MAEl2WxrN6O61ScXa9Uha IsRX4z36v/9hR/3M/BxvO90SgLxuaGuGOxQX8xamtxGCOGY2zcX1wSzJ+gfr7dHZ55Tq 0kiyE8yYqE/JEp/tC60ph/kiTduYIVR9D4FUUuLGYSmKG9GUUjA37WFcuYz/i6hVkKyN hrTZjU9V6SvrBZG5Ekztq8dn+rL2UJagnJetUrowTQ8xr/XwR2DSzfFtgSVOEf9PPXSn 8Mnw== X-Forwarded-Encrypted: i=1; AJvYcCUdcW0VfYOXem7xaefCTSFEofPeVDBQpvT5jw4pUwE0H7HQSBN5uWu5Ov6M69IoSCr+vwJqTKSWJDGj@nongnu.org X-Gm-Message-State: AOJu0Yy+flhHCA0I2sprYAJwG4m2+Z5iy02rNxMXVH4jNrOwCkvu8CNl QNP/dAMvXxOMdZSV3sqkBybDJFYdk4HB5878e0kf1sTl1QTTiuTldneJyXoS17fs X-Gm-Gg: ASbGncsRitTKKeqQ/OrVreCDvtMrPChKKX1OlUetdtTEgRZC+ajjcpvpHxHh2M5b/6H hJhDmYIvI403KQNJfbJjb6Gic/P3VR4Se7QBYibbs/WohMSlpA/AAig3mTzrI2h2RsKpJLmXjsD dIntXJv+mHoOV7OtLj3DEfHi6E7tjTJ66aBHiy88ZwygSJgfnu5Xn0fbXeaf6rHBLvOYrxxB/KU 887kNO8PR8kDYysIguoKpSok+PrPhcrBZqv/czC6folb7kxs7fw1DB9ML+ISas5r8vjLH7SdOcF GTOUSQpSI3t9KFdQUev8Lf85q8cwJoTIBX3pwvSseDiiOs5Rb8jSUlgFkvCdbVlYRxuetL4B68l lnI2aMagW34gz9R0bTDNE9wvRuyb8JmnbIuIS3MMgG4tPcf71E1/njus/9fbswDsQJzWdGUrJki z6Qv3MIaAgvQ== X-Google-Smtp-Source: AGHT+IEDVo47DbeBWfQC+qCr0SIZsS/rbEQO8X1f/71St4548tp5XGzMg04Y490MKDvzWbeN52w0ig== X-Received: by 2002:a05:600c:a46:b0:456:1bca:7faf with SMTP id 5b1f17b1804b1-45b517ad840mr251749995e9.16.1756332089058; Wed, 27 Aug 2025 15:01:29 -0700 (PDT) From: Leonid Bloch 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@nongnu.org Subject: [PATCH v3 1/4] hw/acpi: Support extended GPE handling for additional ACPI devices Date: Thu, 28 Aug 2025 01:00:47 +0300 Message-ID: <20250827220054.37268-2-lb.workbox@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20250827220054.37268-1-lb.workbox@gmail.com> References: <20250827220054.37268-1-lb.workbox@gmail.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=2a00:1450:4864:20::32a; envelope-from=lb.workbox@gmail.com; helo=mail-wm1-x32a.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @gmail.com) X-ZM-MESSAGEID: 1756332182586116600 Content-Type: text/plain; charset="utf-8" This patch extends the GPE (General Purpose Event) handling to support the maximum number of interrupts available based on the machine's GPE register length, rather than being limited to the first 8 bits. This change is needed to support additional ACPI devices that will be introduced in subsequent patches (Battery, AC adapter, and button devices). These new devices require GPE event bits beyond the first 8, which were previously not being properly handled by the event sending and SCI (System Control Interrupt) update mechanisms. The actual number of available GPE interrupts varies by machine type: - PIIX4: GPE_LEN =3D 4 (32 bits total across status and enable registers) - ICH9: ICH9_PMIO_GPE0_LEN =3D 16 (128 bits total) The patch modifies: - acpi_send_gpe_event(): Now properly propagates status bits across all available GPE registers based on the machine's gpe.len value - acpi_update_sci(): Checks all GPE registers for pending interrupts, not just the first byte Note: A future enhancement could refactor the GPE handling to use the bitmap API from bitops.h instead of the current manual bit manipulation, which would provide a cleaner interface for these operations. Signed-off-by: Leonid Bloch --- 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..3240ec185e 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] |=3D status; + int i; + AcpiEventStatusBits st =3D status; + + for (i =3D 0; i < ar->gpe.len / 2; i++) { + ar->gpe.sts[i] |=3D st; + st >>=3D TYPE_WIDTH(ar->gpe.sts[0]); + } + acpi_update_sci(ar, irq); } =20 void acpi_update_sci(ACPIREGS *regs, qemu_irq irq) { int sci_level, pm1a_sts; + bool gpe_sci =3D false; + int i; =20 pm1a_sts =3D acpi_pm1_evt_get_sts(regs); =20 + for (i =3D 0; i < regs->gpe.len / 2; i++) { + gpe_sci =3D gpe_sci || !!(regs->gpe.sts[i] & regs->gpe.en[i]); + } + sci_level =3D ((pm1a_sts & regs->pm1.evt.en & ACPI_BITMASK_PM1_COMMON_ENABLED) !=3D= 0) || - ((regs->gpe.sts[0] & regs->gpe.en[0]) !=3D 0); + gpe_sci; =20 qemu_set_irq(irq, sci_level); =20 --=20 2.51.0 From nobody Sun Sep 28 15:29:10 2025 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=none dis=none) header.from=gmail.com ARC-Seal: i=1; a=rsa-sha256; t=1756332162; cv=none; d=zohomail.com; s=zohoarc; b=f8e9cdanAygzT6ZnqZ9bUNSnqeOVkAzEye2XUV7fPR2Z7lEY/cCYP5ZT9Ge/kv5h/u9vUsZLnwIsVOHzQU2S6WvYvYxPUbp2EnKIFXXb8aJ4yHG4ANtyDd5Di+/DLVWXej4SOTlVan37DXdHKfU6waNTscQBiqv6S8cBz4MIvMk= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1756332162; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=gYvs69YvaoQ23YnMzPDPvXYRbNh+Cz2sFGn5seOfGC8=; b=WnD78sAwaX9VzUTeEAxZcHkr0egXUjvOxoBXXvqi3X+kDiFhawIHVQQ5tkk6C8mf8dm19yQFjFimYEI5bNdCrPvjOYJMeg5bq+gc9trQW5aEWQgkcq3YYsxKbYFj0sBlgef13wJmfsr/iJsu5zCsyv75Pk6b1pcertYesQZJ2Zw= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1756332162628160.4795540467751; Wed, 27 Aug 2025 15:02:42 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1urOD7-0006ib-Mu; Wed, 27 Aug 2025 18:01:41 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1urOD3-0006gY-KL for qemu-devel@nongnu.org; Wed, 27 Aug 2025 18:01:38 -0400 Received: from mail-wm1-x32d.google.com ([2a00:1450:4864:20::32d]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1urOCz-0007eR-74 for qemu-devel@nongnu.org; Wed, 27 Aug 2025 18:01:37 -0400 Received: by mail-wm1-x32d.google.com with SMTP id 5b1f17b1804b1-45a1b05a59fso2185965e9.1 for ; Wed, 27 Aug 2025 15:01:32 -0700 (PDT) Received: from localhost.localdomain (46-116-237-160.bb.netvision.net.il. [46.116.237.160]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-45b797ce2b8sm4116755e9.12.2025.08.27.15.01.29 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 27 Aug 2025 15:01:30 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1756332091; x=1756936891; darn=nongnu.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=gYvs69YvaoQ23YnMzPDPvXYRbNh+Cz2sFGn5seOfGC8=; b=AXlj1aAHoOEgfuaIqX7Q+5H8nA82YSTHc743mYB3R11kHGgmr/AVBMh9f4BPnL4+38 6Iqzi8qgo5yxdVlj+BYcuM65/zwSwBam9mn8Gl8ZTTZbE0uuT/i38BRplhy3jTrplfeI MA4TMtK09WpBq/fnFD9j7kHWocm8HE9y9eeOjuSXB1x/weztfrOnTAcHy3pA+m1FaC7d RmVj4UTG07IPsvWRPhXU4/PRUz/dC8cieIAvsn3TiNJcaBhOhIze/cIfZFljhzvGSaQ7 8in1LJXLkWW7JICCxwIDvXiCmAhxuF3RRlYy6r7qMvBfmiwZrEx8juliO/MnkR1bqCHn Ojbg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1756332091; x=1756936891; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=gYvs69YvaoQ23YnMzPDPvXYRbNh+Cz2sFGn5seOfGC8=; b=ecIv1BiTXLwXh0TWLesg5uOJTiGJzr4Ssxenv/NFIMgAyjhQ2IENA7kYqThFxyzq+C MbzrQvmNBjkuRSy/hbm7OdBr4ZPLgkhESKO8Zq7ravpZ2iRP9om20A/fm84Bv7uJxkUW HAJ0cCIYf4S0y/LPHzjxNo8w/yIz4ck9faEZNbjch9jfpJhjZl0bkwOggJEjYOn+PKrO O+kIAWVDGaaxUp2zg6VhbonRZLI7Fp1+h5+t7aRV1A/D+Cme433RqkPgykUmE1Ftjhjn WWTY8DW1YFq6w4pad0v68x0oB/h0K+TqLbrm8asXDMOElVzePEMz75dqBNDigU7Ez//b jYcQ== X-Forwarded-Encrypted: i=1; AJvYcCVRAcyj5U4wnh9Uqd3aRqYmuRRpRuTj7mrGHBylO+T2qtYsfl8cMeU3WVtRkKI1AWsbu1QY2AZK8n7j@nongnu.org X-Gm-Message-State: AOJu0YwgUvGXkXbjSUfl8z0uzNPHgGksZeUe8058tohO8NX7mg00/qyz 87vUHiSY49BwkTcuLn0Sa/U0b+ZfKTyU4RXaiOA5NL/KS1qOb78Nn+i8 X-Gm-Gg: ASbGncsrqzHbz9W8TkWmhixP3wTjGPcnxR4iG59JPG+Jf3+Lhyzu+RfAezHT0rSpvhh LmY8KdBnwauBlPQ4v4ZeIPsIvL15dmzHQ4VzbDuMOrGm/C7i9Zl0hFUZPmnix8uLvieDLBRRCxH yBmAyeWvr64JIWbKctEh1tEfP2hbGhqzsHRWMX3bsCzzFJiL+VyI6HRV9gBjRFk72KQI/PWI0Aw G7z/NmFD4FKBlrjxBOl9uepeEiTmBEbIFtm52lQYtjpJhETd5gOfrPq6ELscyJoKbXdKDPqX9N1 /sANA2ZUGa37ZeB3ed/vzO3oCJ1wXW+HnvTm8EPD1pbKIDMt6qzaRdaIhvnhJ/ksQitkrYefd2w CHW+hZo0T3Gp2KDzSpHCCxyo79TvM+4F0TVKZncWluEe9Njf9e+GFbb7N5EgIut9z2cIUXTk7GT gqXI0zo4I+Nw== X-Google-Smtp-Source: AGHT+IFKnzjaoZxpMwHKdxszAdzpHDrj0fZxftK3pyg37QZpuONjY7GuuIPrbllH8iDgXlaoU/VWkA== X-Received: by 2002:a05:600c:4f03:b0:458:a7b5:9f6c with SMTP id 5b1f17b1804b1-45b5ec68358mr122273245e9.11.1756332091008; Wed, 27 Aug 2025 15:01:31 -0700 (PDT) From: Leonid Bloch 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@nongnu.org Subject: [PATCH v3 2/4] hw/acpi: Introduce the QEMU Battery Date: Thu, 28 Aug 2025 01:00:48 +0300 Message-ID: <20250827220054.37268-3-lb.workbox@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20250827220054.37268-1-lb.workbox@gmail.com> References: <20250827220054.37268-1-lb.workbox@gmail.com> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=2a00:1450:4864:20::32d; envelope-from=lb.workbox@gmail.com; helo=mail-wm1-x32d.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @gmail.com) X-ZM-MESSAGEID: 1756332168757124100 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 Signed-off-by: Marcel Apfelbaum --- 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 a64b5b849b..eb71a4a4b7 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2928,6 +2928,12 @@ F: hw/vmapple/* F: include/hw/vmapple/* F: docs/system/arm/vmapple.rst =20 +Battery +M: Leonid Bloch +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 + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +Battery Device +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +The battery device provides battery state information to the guest. It sup= ports +two operating modes: + +1. **QMP Control Mode** (default): Battery state is controlled via QMP com= mands, + providing deterministic control for testing and migration safety. +2. **Sysfs Mode**: Battery state mirrors the host's physical battery, usef= ul + for desktop virtualization where the guest should see the host's batter= y. + +Configuration +------------- + +The battery device is created as an ISA device using ``-device battery``. + +Operating Modes +~~~~~~~~~~~~~~~ + +**QMP Control Mode** (``use-qmp=3Dtrue``, default) + Battery state is controlled via QMP commands. This mode is recommended f= or: + + * Production environments requiring migration support + * Testing with predictable battery states + * Environments without host battery access + * Security-sensitive deployments + +**Sysfs Mode** (``enable-sysfs=3Dtrue``) + 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=3Dtrue``. + +``enable-sysfs`` (default: false) + Enable sysfs mode to mirror the host's battery. Cannot be used together + with ``use-qmp=3Dtrue``. + +``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 =3D Discharging + * 2 =3D 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 availabl= e: + +``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= =3Doff + + # 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=3Dfalse,enable-sysfs=3Dtrue + + # Custom probe interval (5 seconds) + qemu-system-x86_64 -device battery,use-qmp=3Dfalse,enable-sysfs=3Dtrue,p= robe_interval=3D5000 + + # Specific battery path + qemu-system-x86_64 -device battery,use-qmp=3Dfalse,enable-sysfs=3Dtrue,s= ysfs_path=3D/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 =CE= =BCWh + echo "3000000" > /tmp/fake_battery/energy_full # Full capacity in =CE= =BCWh + echo "500000" > /tmp/fake_battery/power_now # Current power in =CE= =BCW + + # Use fake battery in sysfs mode + qemu-system-x86_64 -device battery,use-qmp=3Dfalse,enable-sysfs=3Dtrue,s= ysfs_path=3D/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 m= W) 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 + * Marcel Apfelbaum + * Dmitry Fleytman + * + * 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 =3D=3D 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[] =3D { "energy_full", "charge_full" }; +static const char *now_file[] =3D { "energy_now", "charge_now" }; +static const char *rate_file[] =3D { "power_now", "current_now" }; + +static const char *stat_file =3D "status"; +static const char *type_file =3D "type"; + +static const char *discharging_states[] =3D { "Discharging", "Not charging= " }; +static const char *charging_states[] =3D { "Charging", "Full", "Unknown" }; + +static inline bool battery_file_accessible(char *path, const char *file) +{ + char full_path[PATH_MAX]; + int path_len; + + path_len =3D snprintf(full_path, PATH_MAX, "%s/%s", path, file); + if (path_len < 0 || path_len >=3D PATH_MAX) { + return false; + } + if (access(full_path, R_OK) =3D=3D 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 =3D snprintf(file_path, PATH_MAX, "%s/%s", s->bat_path, + full_file[s->units]); + if (path_len < 0 || path_len >=3D PATH_MAX) { + error_setg(errp, "Full capacity file path is inaccessible."); + return; + } + + ff =3D fopen(file_path, "r"); + if (ff =3D=3D NULL) { + error_setg_errno(errp, errno, "Could not read the full charge file= ."); + return; + } + + if (fscanf(ff, "%u", &val) !=3D 1) { + error_setg(errp, "Full capacity undetermined."); + return; + } else { + s->charge_full =3D val; + } + fclose(ff); +} + +static inline bool battery_is_discharging(char *val) +{ + static const int discharging_len =3D ARRAY_SIZE(discharging_states); + int i; + + for (i =3D 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 =3D ARRAY_SIZE(charging_states); + int i; + + for (i =3D 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 =3D snprintf(file_path, PATH_MAX, "%s/%s", s->bat_path, stat_= file); + if (path_len < 0 || path_len >=3D PATH_MAX) { + warn_report("Could not read the battery state."); + return; + } + + ff =3D fopen(file_path, "r"); + if (ff =3D=3D NULL) { + warn_report("Could not read the battery state."); + return; + } + + if (fgets(val, MAX_ALLOWED_STATE_LENGTH, ff) =3D=3D NULL) { + warn_report("Battery state unreadable."); + } else { + val[strcspn(val, "\n")] =3D 0; + if (battery_is_discharging(val)) { + s->state.val =3D BATTERY_DISCHARGING; + } else if (battery_is_charging(val)) { + s->state.val =3D BATTERY_CHARGING; + } else { + s->state.val =3D 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 =3D snprintf(file_path, PATH_MAX, "%s/%s", s->bat_path, + rate_file[s->units]); + if (path_len < 0 || path_len >=3D PATH_MAX) { + warn_report("Could not read the battery rate."); + s->rate.val =3D BATTERY_VAL_UNKNOWN; + return; + } + + ff =3D fopen(file_path, "r"); + if (ff =3D=3D NULL) { + warn_report("Could not read the battery rate."); + s->rate.val =3D BATTERY_VAL_UNKNOWN; + return; + } + + if (fscanf(ff, "%lu", &val) !=3D 1) { + warn_report("Battery rate undetermined."); + s->rate.val =3D BATTERY_VAL_UNKNOWN; + } else { + s->rate.val =3D 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 =3D snprintf(file_path, PATH_MAX, "%s/%s", s->bat_path, + now_file[s->units]); + if (path_len < 0 || path_len >=3D PATH_MAX) { + warn_report("Could not read the battery charge."); + s->charge.val =3D BATTERY_VAL_UNKNOWN; + return; + } + + ff =3D fopen(file_path, "r"); + if (ff =3D=3D NULL) { + warn_report("Could not read the battery charge."); + s->charge.val =3D BATTERY_VAL_UNKNOWN; + return; + } + + if (fscanf(ff, "%lu", &val) !=3D 1) { + warn_report("Battery charge undetermined."); + s->charge.val =3D BATTERY_VAL_UNKNOWN; + } else { + s->charge.val =3D 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 =3D 0; + if (s->qmp_present) { + if (s->qmp_charging) { + s->state.val |=3D BATTERY_CHARGING; + } + if (s->qmp_discharging) { + s->state.val |=3D BATTERY_DISCHARGING; + } + } + s->rate.val =3D s->qmp_rate; + s->charge.val =3D (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 =3D 0; + s->rate.val =3D 0; + s->charge.val =3D 0; + } + + trace_battery_get_dynamic_status(s->state.val, s->rate.val, s->charge.= val); +} + +static void battery_probe_state(void *opaque) +{ + BatteryState *s =3D opaque; + + uint32_t state_before =3D s->state.val; + uint32_t rate_before =3D s->rate.val; + uint32_t charge_before =3D s->charge.val; + + battery_get_dynamic_status(s); + + if (state_before !=3D s->state.val || rate_before !=3D s->rate.val || + charge_before !=3D s->charge.val) { + Object *obj =3D 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 =3D timer_new_ms(QEMU_CLOCK_VIRTUAL, + battery_probe_state, s); + timer_mod(s->probe_state_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTU= AL) + + 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 =3D snprintf(type_path, PATH_MAX, "%s/%s", path, type_file); + if (path_len < 0 || path_len >=3D PATH_MAX) { + return false; + } + ff =3D fopen(type_path, "r"); + if (ff =3D=3D NULL) { + return false; + } + + if (fgets(val, MAX_ALLOWED_STATE_LENGTH, ff) =3D=3D NULL) { + fclose(ff); + return false; + } else { + val[strcspn(val, "\n")] =3D 0; + if (strncmp(val, BATTERY_TYPE, MAX_ALLOWED_STATE_LENGTH)) { + fclose(ff); + return false; + } + } + fclose(ff); + + units =3D battery_select_file(path, full_file); + + if (units < 0) { + return false; + } else { + s->units =3D 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 =3D 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 =3D opendir(SYSFS_PATH); + if (dir =3D=3D NULL) { + return false; + } + + ent =3D readdir(dir); + while (ent !=3D NULL) { + if (ent->d_name[0] !=3D '.') { + path_len =3D snprintf(bp, PATH_MAX, "%s/%s", SYSFS_PATH, + ent->d_name); + if (path_len < 0 || path_len >=3D PATH_MAX) { + return false; + } + if (battery_verify_sysfs(s, bp)) { + qdev_prop_set_string(dev, BATTERY_PATH_PROP, bp); + closedir(dir); + return true; + } + } + ent =3D readdir(dir); + } + closedir(dir); + + return false; +} + +static void battery_realize(DeviceState *dev, Error **errp) +{ + ISADevice *d =3D ISA_DEVICE(dev); + BatteryState *s =3D BATTERY_DEVICE(dev); + FWCfgState *fw_cfg =3D fw_cfg_find(); + uint16_t *battery_port; + char err_details[0x20] =3D {}; + + 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 =3D true; + s->qmp_charging =3D false; + s->qmp_discharging =3D true; + s->qmp_charge_percent =3D 50; + s->qmp_rate =3D 1000; /* 1000 mW discharge rate */ + } + + if (s->enable_sysfs) { + if (!s->bat_path) { + strcpy(err_details, " Try using 'sysfs_path=3D'"); + } + + 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 =3D BATTERY_FULL_CAP; + } + + isa_register_ioport(d, &s->io, s->ioport); + + battery_probe_state_timer_init(s); + + if (!fw_cfg) { + return; + } + + battery_port =3D g_malloc(sizeof(*battery_port)); + *battery_port =3D 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[] =3D { + 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 =3D { + .name =3D "battery", + .version_id =3D 1, + .minimum_version_id =3D 1, + .fields =3D (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 =3D BATTERY_DEVICE(adev); + + bat_state =3D aml_local(0); + bat_rate =3D aml_local(1); + bat_charge =3D aml_local(2); + + sb_scope =3D aml_scope("\\_SB"); + dev =3D aml_device("BAT0"); + aml_append(dev, aml_name_decl("_HID", aml_eisaid("PNP0C0A"))); + + method =3D 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 =3D 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 =3D aml_method("_BIF", 0, AML_NOTSERIALIZED); + pkg =3D 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 =3D 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 =3D 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 =3D 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 =3D 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 =3D 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 =3D DEVICE_CLASS(class); + AcpiDevAmlIfClass *adevc =3D ACPI_DEV_AML_IF_CLASS(class); + + dc->realize =3D battery_realize; + device_class_set_props(dc, battery_device_properties); + dc->vmsd =3D &battery_vmstate; + adevc->build_dev_aml =3D build_battery_aml; +} + +static uint64_t battery_ioport_read(void *opaque, hwaddr addr, unsigned si= ze) +{ + BatteryState *s =3D 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 =3D { + .read =3D battery_ioport_read, + .impl =3D { + .min_access_size =3D 1, + .max_access_size =3D 1, + }, +}; + +static void battery_instance_init(Object *obj) +{ + BatteryState *s =3D BATTERY_DEVICE(obj); + + memory_region_init_io(&s->io, obj, &battery_ops, s, "battery", + BATTERY_LEN); +} + +static const TypeInfo battery_info =3D { + .name =3D TYPE_BATTERY, + .parent =3D TYPE_ISA_DEVICE, + .instance_size =3D sizeof(BatteryState), + .class_init =3D battery_class_init, + .instance_init =3D battery_instance_init, + .interfaces =3D (InterfaceInfo[]) { + { TYPE_ACPI_DEV_AML_IF }, + { }, + }, +}; + +static BatteryState *find_battery_device(void) +{ + Object *o =3D 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 =3D find_battery_device(); + + if (!s) { + error_setg(errp, "No battery device found"); + return; + } + + s->qmp_present =3D state->present; + s->qmp_charging =3D state->charging; + s->qmp_discharging =3D state->discharging; + s->qmp_charge_percent =3D state->charge_percent; + + if (state->has_rate) { + s->qmp_rate =3D state->rate; + } + + Object *obj =3D 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 =3D find_battery_device(); + BatteryInfo *ret; + + if (!s) { + error_setg(errp, "No battery device found"); + return NULL; + } + + ret =3D g_new0(BatteryInfo, 1); + + if (s->use_qmp_control) { + ret->present =3D s->qmp_present; + ret->charging =3D s->qmp_charging; + ret->discharging =3D s->qmp_discharging; + ret->charge_percent =3D s->qmp_charge_percent; + ret->has_rate =3D true; + ret->rate =3D s->qmp_rate; + } else { + battery_get_dynamic_status(s); + ret->present =3D true; + ret->charging =3D !!(s->state.val & BATTERY_CHARGING); + ret->discharging =3D !!(s->state.val & BATTERY_DISCHARGING); + ret->charge_percent =3D (s->charge.val * 100) / BATTERY_FULL_CAP; + ret->has_rate =3D true; + ret->rate =3D s->rate.val; + } + + ret->has_remaining_capacity =3D false; + ret->has_design_capacity =3D true; + ret->design_capacity =3D 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-bui= ld-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%" P= RIx64 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, =20 aml_append(sb_scope, dev); } + aml_append(dsdt, sb_scope); =20 if (pm->pcihp_bridge_en || pm->pcihp_root_en) { diff --git a/include/hw/acpi/acpi_dev_interface.h b/include/hw/acpi/acpi_de= v_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 =3D 16, ACPI_VMGENID_CHANGE_STATUS =3D 32, ACPI_POWER_DOWN_STATUS =3D 64, + ACPI_BATTERY_CHANGE_STATUS =3D 128, } AcpiEventStatusBits; =20 #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 + * Marcel Apfelbaum + * Dmitry Fleytman + * + * 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' } --=20 2.51.0 From nobody Sun Sep 28 15:29:10 2025 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=none dis=none) header.from=gmail.com ARC-Seal: i=1; a=rsa-sha256; t=1756332191; cv=none; d=zohomail.com; s=zohoarc; b=T0VBt48Up7iMefqMuNcLTcZPW+HT1pCVlI7ZU17NCFB8GoClFi5YI0y+FfW5zHJ90ZOfYY7hq6SMnmbuyiqmkU11rsNe7USlNirQBzKiKvBsL75+5uAAgyljl68bf6VflipNZQZBWHfc6Xt6/iikQNYT3Q3L852CZmnBnKXzHDg= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1756332191; h=Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=1CMa98DanhdU+YFxRVKMRC0axlRfKE91aptJPg8KRCU=; b=RvQKnnu1wP58SLjJMYRSZ0DJc2MfPcXHEeR9qSmWGOUqWaUXikgaTFEsamuccFOGgJbzA538nkS1TpSWTgkNE3f1N8x9dmL/8QipRVRxDYE/dHjw0jvEEJtvRM+qpZOQOZz55UvWXoeO2rL7VFYtwu97YT802Y1raN4DqiEN+6w= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1756332191061655.8289505095242; Wed, 27 Aug 2025 15:03:11 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1urODA-0006kW-11; Wed, 27 Aug 2025 18:01:44 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1urOD5-0006hL-Oy for qemu-devel@nongnu.org; Wed, 27 Aug 2025 18:01:40 -0400 Received: from mail-wm1-x333.google.com ([2a00:1450:4864:20::333]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1urOD0-0007eo-U3 for qemu-devel@nongnu.org; Wed, 27 Aug 2025 18:01:38 -0400 Received: by mail-wm1-x333.google.com with SMTP id 5b1f17b1804b1-45a1b00f23eso1729405e9.0 for ; Wed, 27 Aug 2025 15:01:34 -0700 (PDT) Received: from localhost.localdomain (46-116-237-160.bb.netvision.net.il. [46.116.237.160]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-45b797ce2b8sm4116755e9.12.2025.08.27.15.01.31 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 27 Aug 2025 15:01:32 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1756332093; x=1756936893; darn=nongnu.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=1CMa98DanhdU+YFxRVKMRC0axlRfKE91aptJPg8KRCU=; b=NasbsWhnmfo9S792e4qB6SpDqymMg0k5EPbd5yjYz4D8PnyxmSvXbcLfJj5H2s8fXy qdO5jsMmphdiYDgXtM6UpOuZeXILCZLCDXY2gQWdo/P6XLKi+SpO1P5Ng4bQK265XuiS whqAiAL35PVkkqy1NfeBLvqrKLfjzIDY7581ShHSPXAyLBSU4zjQ0IrlPCQhP/7wLHnt 6zhPRLYrHJe0FIK/IwObjmyIOxWucZuNCM3YLDMJ44A1HgJB9kTY5Pws2Q28ahwhTY7k gcxKtGegPumXI7k88zchEAG7f4ieMzvQ1Kvt/FHWzWpZenuY+hEpnoNbS7l7UEVMFiPQ 2YCw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1756332093; x=1756936893; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=1CMa98DanhdU+YFxRVKMRC0axlRfKE91aptJPg8KRCU=; b=rjfZAHOpnDiRHqk0EonYVbMCB3BIl5gy5w2HzIuChaob/xTYUGCp4RUGxHjH657tLF md5jzDrW7tvo0/DIIt06NlsDXmBl1VkSnvWGxe6si1YgWWwdj+0fxqmLch3V0kJTEYzt kX5GgqjFlDAuLcX6wfGhiFXJgD/KkB+EP6fB57bKbuLvaesVFsujF/Hgr9kIMwZWihhw oKVcXFqgq7naKgwnTGoqV4QX6m34rBIp7A9vHiiHPGnxsJQD4Qbb3DRUrviEZGUNb8EN pTySt2Vq0ASbgkjruPGB/mvXnJ6yQfHF6RiISY7QPFG1Z4yLrxMmdKklRD75DbwEn3aB eIBw== X-Forwarded-Encrypted: i=1; AJvYcCUdscgLcUEL2BpfFZaFYQK8PRr6PxFfIDdhktjZJlME/tuiCd9b0XXu4PmrkEYRP/4sw4bjFSjAmhRA@nongnu.org X-Gm-Message-State: AOJu0YyBGlkNONdwEFLzIuA82/A2NYeZKmIwcRFc0Yop+qtTT/rNpfCg W0Uxj8sf5h2KzBe38MbzjiAVmClOCvjO0sETa7/7Qbz+PY4ajeamRQ4b X-Gm-Gg: ASbGncse8wDonvighdP4rQ7D2O0cEggyqj+iqQppDy57O3qq4h+tKCuloc0+0z7mplI ZPTIJZpsKDYNN8aNiyVWAlPrUajjugj/6CndiGG9HWI5ecrr8YN/PZGAI40UY8LKwoO9PoMLZc2 lmSHkhqpYkzR1vZoZl7jfzaMBEt8/1+WmMzZOch7YZo8VyayKWauYYT3ZbLvzdGZRDLUAJYuVic RMNZtfvAJP3OF9MP5AgaqJS7nyoA0TF6NX4lDUElhQjubnaaBSoa3pjm9EB+wr+iAV62CTnAJfH eKe5aj0/yI4iRfP2JL1wufMfZxzU9r7C9J9O98oi+3sAeaySkEBRzYOzWZVsicO5fSNB9/oD7c2 4bIBhcXRVRO9NBUpt/qG1ho297TUFWPSOlcP9NzWTi0o+l7T6dClP3i2lt/HiMca9Vogj9XjC8I o1V7O1M82V297JhDMI8dit X-Google-Smtp-Source: AGHT+IHOvQsfIKMoHlVOgOawz7UfpaHS0zY4OCqDIQ7ZwQ8nDXAsEuD9vYaYnMcHftbWGrxf6LzcmA== X-Received: by 2002:a05:600c:1d0c:b0:456:fc1:c286 with SMTP id 5b1f17b1804b1-45b66eaddc9mr56646205e9.1.1756332092860; Wed, 27 Aug 2025 15:01:32 -0700 (PDT) From: Leonid Bloch 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@nongnu.org Subject: [PATCH v3 3/4] hw/acpi: Introduce the QEMU AC adapter Date: Thu, 28 Aug 2025 01:00:49 +0300 Message-ID: <20250827220054.37268-4-lb.workbox@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20250827220054.37268-1-lb.workbox@gmail.com> References: <20250827220054.37268-1-lb.workbox@gmail.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=2a00:1450:4864:20::333; envelope-from=lb.workbox@gmail.com; helo=mail-wm1-x333.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @gmail.com) X-ZM-MESSAGEID: 1756332192718116600 Content-Type: text/plain; charset="utf-8" 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 --- 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 eb71a4a4b7..19bea634c7 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2934,6 +2934,12 @@ S: Maintained F: hw/acpi/battery.* F: docs/specs/battery.rst =20 +AC Adapter +M: Leonid Bloch +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 + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +AC Adapter Device +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +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 saf= ety. +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=3Dtrue``, default) + AC adapter state is controlled via QMP commands. This mode is recommende= d for: + + * Production environments requiring migration support + * Testing with predictable power states + * Environments without host AC adapter access + * Security-sensitive deployments + +**Sysfs Mode** (``enable-sysfs=3Dtrue``) + AC adapter mirrors the host's physical AC adapter. This mode is useful f= or: + + * 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=3Dtrue``. + +``enable-sysfs`` (default: false) + Enable sysfs mode to mirror the host's AC adapter. Cannot be used togeth= er + with ``use-qmp=3Dtrue``. + +``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 defa= ult, + 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 =3D offline, 1 =3D 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 availabl= e: + +``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=3Doff + + # 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=3Dfalse,enable-sysfs=3Dtrue + + # Custom probe interval (5 seconds) + qemu-system-x86_64 -device acad,use-qmp=3Dfalse,enable-sysfs=3Dtrue,prob= e_interval=3D5000 + + # Specific AC adapter path + qemu-system-x86_64 -device acad,use-qmp=3Dfalse,enable-sysfs=3Dtrue,sysf= s_path=3D/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 =3D connected, 0 =3D disconn= ected + + # Use fake AC adapter in sysfs mode + qemu-system-x86_64 -device acad,use-qmp=3Dfalse,enable-sysfs=3Dtrue,sysf= s_path=3D/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=3Dfalse,enable-sysfs=3Dtrue \ + -device acad,use-qmp=3Dfalse,enable-sysfs=3Dtrue 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 =20 +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 + * Marcel Apfelbaum + * Dmitry Fleytman + * + * 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 =3D 0, + AC_ADAPTER_ONLINE =3D 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 =3D "online"; +static const char *type_file =3D "type"; + +static inline bool acad_file_accessible(char *path, const char *file) +{ + char full_path[PATH_MAX]; + int path_len; + + path_len =3D snprintf(full_path, PATH_MAX, "%s/%s", path, file); + if (path_len < 0 || path_len >=3D PATH_MAX) { + return false; + } + + if (access(full_path, R_OK) =3D=3D 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 =3D snprintf(file_path, PATH_MAX, "%s/%s", s->acad_path, + online_file); + if (path_len < 0 || path_len >=3D PATH_MAX) { + warn_report("Could not read the AC adapter state."); + return; + } + + ff =3D fopen(file_path, "r"); + if (ff =3D=3D 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 =3D 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 =3D s->qmp_connected ? AC_ADAPTER_ONLINE : AC_ADAPTER_OFF= LINE; + } else if (s->enable_sysfs) { + acad_get_state(s); + } else { + s->state =3D AC_ADAPTER_OFFLINE; + } + + trace_acad_get_dynamic_status(s->state); +} + +static void acad_probe_state(void *opaque) +{ + ACADState *s =3D opaque; + + uint8_t state_before =3D s->state; + + acad_get_dynamic_status(s); + + if (state_before !=3D s->state) { + Object *obj =3D 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 =3D timer_new_ms(QEMU_CLOCK_VIRTUAL, + acad_probe_state, s); + timer_mod(s->probe_state_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTU= AL) + + 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 =3D snprintf(type_path, PATH_MAX, "%s/%s", path, type_file); + if (path_len < 0 || path_len >=3D PATH_MAX) { + return false; + } + + ff =3D fopen(type_path, "r"); + if (ff =3D=3D NULL) { + return false; + } + + if (fgets(val, MAX_ALLOWED_TYPE_LENGTH, ff) =3D=3D NULL) { + fclose(ff); + return false; + } else { + val[strcspn(val, "\n")] =3D 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 =3D 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 =3D opendir(SYSFS_PATH); + if (dir =3D=3D NULL) { + return false; + } + + ent =3D readdir(dir); + while (ent !=3D NULL) { + if (ent->d_name[0] !=3D '.') { + path_len =3D snprintf(bp, PATH_MAX, "%s/%s", SYSFS_PATH, + ent->d_name); + if (path_len < 0 || path_len >=3D 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 =3D readdir(dir); + } + closedir(dir); + + return false; +} + +static void acad_realize(DeviceState *dev, Error **errp) +{ + ISADevice *d =3D ISA_DEVICE(dev); + ACADState *s =3D AC_ADAPTER_DEVICE(dev); + FWCfgState *fw_cfg =3D fw_cfg_find(); + uint16_t *acad_port; + char err_details[32] =3D {}; + + 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=3D'"); + } + + if (!get_acad_path(dev)) { + error_setg(errp, "AC adapter sysfs path not found or unreadabl= e.%s", + err_details); + return; + } + } + + isa_register_ioport(d, &s->io, s->ioport); + + acad_probe_state_timer_init(s); + + if (!fw_cfg) { + return; + } + + acad_port =3D g_malloc(sizeof(*acad_port)); + *acad_port =3D 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[] =3D { + 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 =3D { + .name =3D "acad", + .version_id =3D 1, + .minimum_version_id =3D 1, + .fields =3D (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 =3D AC_ADAPTER_DEVICE(adev); + + acad_state =3D aml_local(0); + + sb_scope =3D aml_scope("\\_SB"); + dev =3D 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 =3D aml_field("ACST", AML_BYTE_ACC, AML_NOLOCK, AML_PRESERVE); + aml_append(field, aml_named_field("PWRS", 8)); + aml_append(dev, field); + + method =3D 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 =3D aml_method("_PCL", 0, AML_NOTSERIALIZED); + pkg =3D aml_package(1); + aml_append(pkg, aml_name("_SB")); + aml_append(method, aml_return(pkg)); + aml_append(dev, method); + + method =3D aml_method("_PIF", 0, AML_NOTSERIALIZED); + pkg =3D 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 =3D 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 =3D DEVICE_CLASS(class); + AcpiDevAmlIfClass *adevc =3D ACPI_DEV_AML_IF_CLASS(class); + + dc->realize =3D acad_realize; + device_class_set_props(dc, acad_device_properties); + dc->vmsd =3D &acad_vmstate; + adevc->build_dev_aml =3D build_acad_aml; +} + +static uint64_t acad_ioport_read(void *opaque, hwaddr addr, unsigned size) +{ + ACADState *s =3D 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 =3D { + .read =3D acad_ioport_read, + .impl =3D { + .min_access_size =3D 1, + .max_access_size =3D 1, + }, +}; + +static void acad_instance_init(Object *obj) +{ + ACADState *s =3D AC_ADAPTER_DEVICE(obj); + + memory_region_init_io(&s->io, obj, &acad_ops, s, "acad", + AC_ADAPTER_LEN); +} + +static const TypeInfo acad_info =3D { + .name =3D TYPE_AC_ADAPTER, + .parent =3D TYPE_ISA_DEVICE, + .instance_size =3D sizeof(ACADState), + .class_init =3D acad_class_init, + .instance_init =3D acad_instance_init, + .interfaces =3D (InterfaceInfo[]) { + { TYPE_ACPI_DEV_AML_IF }, + { }, + }, +}; + +static ACADState *find_acad_device(void) +{ + Object *o =3D 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 =3D find_acad_device(); + + if (!s) { + error_setg(errp, "No AC adapter device found"); + return; + } + + s->qmp_connected =3D connected; + + Object *obj =3D 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 =3D find_acad_device(); + AcAdapterInfo *ret; + + if (!s) { + error_setg(errp, "No AC adapter device found"); + return NULL; + } + + ret =3D g_new0(AcAdapterInfo, 1); + + if (s->use_qmp_control) { + ret->connected =3D s->qmp_connected; + } else { + acad_get_dynamic_status(s); + ret->connected =3D (s->state =3D=3D 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-bui= ld-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 + * Marcel Apfelbaum + * Dmitry Fleytman + * + * 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_de= v_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 =3D 32, ACPI_POWER_DOWN_STATUS =3D 64, ACPI_BATTERY_CHANGE_STATUS =3D 128, + ACPI_AC_ADAPTER_CHANGE_STATUS =3D 1024, } AcpiEventStatusBits; =20 #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' } --=20 2.51.0 From nobody Sun Sep 28 15:29:10 2025 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=none dis=none) header.from=gmail.com ARC-Seal: i=1; a=rsa-sha256; t=1756332187; cv=none; d=zohomail.com; s=zohoarc; b=ldoI2pVhmcCNYepGsNc8Np5757o5c1ylkqFuepwoA2D0i1a7Mf5LBWEcoWgeChfjBGULwOs09BwgDMY96CGHGnUzBN51OMVq/ekYGVdiWf9NuAgN5Pn0YBwlVdIEwAw6LJB5JFWi4jtfJH3otcsqtZaSy6VvveFbTpUSyk37sUU= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1756332187; h=Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=3X0sfvJ9BISpbpisYuLRIvvZ2RbhH0HSOpBQPIHzEpE=; b=FPzA5iZGoZ3Qhzc/8ERLReVxwb9wRHPUZCCEuirE3PZLnyJgXMSWM/HNa6g44CxDqtPV6c8EgfISUZLQPoDUipD8EVmPE97wBOn0W9waF+8fDSZgmOpneHEInXAMwSSwYRe2MtChRqA1kSM00ed1c8NDIgsXzNTpvGaPq/4+EZY= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1756332187727114.15223305741438; Wed, 27 Aug 2025 15:03:07 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1urODA-0006l4-Lw; Wed, 27 Aug 2025 18:01:44 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1urOD8-0006j1-53 for qemu-devel@nongnu.org; Wed, 27 Aug 2025 18:01:42 -0400 Received: from mail-wr1-x42e.google.com ([2a00:1450:4864:20::42e]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1urOD3-0007f0-DQ for qemu-devel@nongnu.org; Wed, 27 Aug 2025 18:01:41 -0400 Received: by mail-wr1-x42e.google.com with SMTP id ffacd0b85a97d-3cd59c5a953so237637f8f.0 for ; Wed, 27 Aug 2025 15:01:37 -0700 (PDT) Received: from localhost.localdomain (46-116-237-160.bb.netvision.net.il. [46.116.237.160]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-45b797ce2b8sm4116755e9.12.2025.08.27.15.01.33 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 27 Aug 2025 15:01:34 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1756332096; x=1756936896; darn=nongnu.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=3X0sfvJ9BISpbpisYuLRIvvZ2RbhH0HSOpBQPIHzEpE=; b=Ju1ykz19Her+ft4JMgn4uh6D4qpBBF4s7WjxFgIVErCESJWCOBYHi3yumFxGtO6jyI pwjAVT6AG5O2k6wi+0kCxxBg9NdFeF4nEXHNzlzwrSlybHIeQNqPZCN2dsLHUvM8tFNq n/WHRy3IbzuNCPgtH8E7aNFjzbslf/On+mQDewEdL9/KKMbZjrl5ZdhPtGQ/R4EoDVOW EowQLLVUXDgXp+2cyukTWW73dZhrdTvGm4uptAZiEytKFN1FvTYBkWcOWGtjXgDHBDhA aTbt9GI8uofjKWlct4kWI+bSkwkWavMhUmwJfMvAmWmqdq8tnQJZDs81lIQUzPi5QtQt H7XA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1756332096; x=1756936896; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=3X0sfvJ9BISpbpisYuLRIvvZ2RbhH0HSOpBQPIHzEpE=; b=XbendQZgi83cxisATbn0oZvk5RawUQYqzKJZCQjyH7GMaIqdKrG6HqhL+2c3d0sYyE zKYBRpc1CYNPj7NqVKoLP3ThmFyZ37SRiogSH5zePQfaNTTutIRLzCCYdqS0FdBWpTuq Cz0EnXUe1mCW8jYrCTIwxck1cN76H5VCFVKgs5hzxN9briWgrdJQBQxrWRg5FhcpsEfG 5LGA3V0JVCY7khIV0P3mmcudtmmFYxBDqi4XcArAlvUAFBCDIWCv0RtMZB2lk5HZNai2 tC+PhTl2SmoXeYlWhqOIT1Fj2IedAKWyphDLzt15hjCRLfElFFRziXSF+5ke+ojnJHmA Vp0g== X-Forwarded-Encrypted: i=1; AJvYcCWqqe5Dxa34QnjOJEyBhFbu7us5OGHNNVDyJ/xDwfgGCcM0GgLNhK9Yy92w47ysUmw+BU86a3NTO3fC@nongnu.org X-Gm-Message-State: AOJu0YxKVjRsp01aQZk4UFSDNykcZcFxCBuKDNhx+60BcDrYkoL54sWM +djSqWY1kGepyOJrkkm21Kkui0bikOm37UY0fESunNeaLlVqkqgGEXvM X-Gm-Gg: ASbGnctxagKPlifAE5ejSTDf6rGel9SpmYQENQmN6jV5Z9DXtWmliGRvsDuuXX3iQoj PBwCaQawMlpz0UaKtbSKGsSI2sAaRp//gnAUvjg+zBgrci8vM0bF40bp+S8f61KpTIlrVKmOKgx Ud2cP16B281EnAVDU5h07NqZ3uDwQD8Y0fOjedOUFTFrvc/4Z4JYV1Cq7vaZyg+f8osZLRTthDo g7FIMCcQj4Xgeatpl85mALC7TjDGuYIIHom38LbX6aVp1JCtxnxQsg7UgMftJsixIzkKQ21Cwbm 9N37K/oRFGm6Gl/pGDj2AamacJzVSMUHj3IG6+iSNpByPEbYJZ288mT1sBDDvHwoN8tUpthqRfG m6y9FNWcElJoSXfA5NFMN8gsIy0vtemcva2mhhpZIrtvoX67zlXP+pS4i2qg8FR4nSUTdPkeF+d o5mmWSLZDxPse+/rPd4xtu X-Google-Smtp-Source: AGHT+IFXwYG5x4tyJa3Pb67+Yky00D86IvuNjyFZ8FkGg6rmY/zaEj8IK3TwpXm9SI+LBX+HJEey4A== X-Received: by 2002:a05:6000:2c0e:b0:3ca:4b59:2714 with SMTP id ffacd0b85a97d-3ca4b592c7fmr9143905f8f.10.1756332095530; Wed, 27 Aug 2025 15:01:35 -0700 (PDT) From: Leonid Bloch 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@nongnu.org Subject: [PATCH v3 4/4] hw/acpi: Introduce the QEMU lid button Date: Thu, 28 Aug 2025 01:00:50 +0300 Message-ID: <20250827220054.37268-5-lb.workbox@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20250827220054.37268-1-lb.workbox@gmail.com> References: <20250827220054.37268-1-lb.workbox@gmail.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=2a00:1450:4864:20::42e; envelope-from=lb.workbox@gmail.com; helo=mail-wr1-x42e.google.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @gmail.com) X-ZM-MESSAGEID: 1756332188828124100 Content-Type: text/plain; charset="utf-8" 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 --- 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 19bea634c7..eb876803eb 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2940,6 +2940,12 @@ S: Maintained F: hw/acpi/acad.* F: docs/specs/acad.rst =20 +Button +M: Leonid Bloch +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 + +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D +Laptop Lid Button Device +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +The button device provides laptop lid button state information to the gues= t. +It supports two operating modes: + +1. **QMP Control Mode** (default): Lid state is controlled via QMP command= s, + providing deterministic control for testing and migration safety. +2. **Procfs Mode**: Lid state mirrors the host's physical lid button, usef= ul + for desktop virtualization where the guest should see the host's lid st= ate. + +Configuration +------------- + +The lid button device is created as an ISA device using ``-device button``. + +Operating Modes +~~~~~~~~~~~~~~~ + +**QMP Control Mode** (``use-qmp=3Dtrue``, 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=3Dtrue``) + 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=3Dtrue``. + +``enable-procfs`` (default: false) + Enable procfs mode to mirror the host's lid button. Cannot be used toget= her + with ``use-qmp=3Dtrue``. + +``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 d= evice + will automatically scan this directory to find the lid state file. Use t= his + 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" ho= st +lid button is to be provided, the ``procfs_path`` property allows overridi= ng +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 =3D closed, 1 =3D 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 availabl= e: + +``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=3D= 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=3Dfalse,enable-procfs=3Dtrue + + # Custom probe interval (5 seconds) + qemu-system-x86_64 -device button,use-qmp=3Dfalse,enable-procfs=3Dtrue,p= robe_interval=3D5000 + + # Custom procfs path + qemu-system-x86_64 -device button,use-qmp=3Dfalse,enable-procfs=3Dtrue,p= rocfs_path=3D/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: "sta= te: open" or "state: closed" + + # Use fake lid button in procfs mode + qemu-system-x86_64 -device button,use-qmp=3Dfalse,enable-procfs=3Dtrue,p= rocfs_path=3D/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=3Dfalse,enable-sysfs=3Dtrue \ + -device acad,use-qmp=3Dfalse,enable-sysfs=3Dtrue \ + -device button,use-qmp=3Dfalse,enable-procfs=3Dtrue 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 =20 +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 + * Marcel Apfelbaum + * Dmitry Fleytman + * + * 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 =3D 0, + LID_OPEN =3D 1, +}; + +static const char *lid_state[] =3D { "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 =3D snprintf(full_path, PATH_MAX, "%s/%s/%s/%s", path, dir, s= ubdir, + file); + if (path_len < 0 || path_len >=3D PATH_MAX) { + return false; + } + + if (access(full_path, R_OK) =3D=3D 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 =3D snprintf(file_path, PATH_MAX, "%s/%s/%s/%s", s->button_pa= th, + LID_DIR, s->lid_dir, LID_STATE_FILE); + if (path_len < 0 || path_len >=3D PATH_MAX) { + warn_report("Could not read the lid state."); + return; + } + + ff =3D fopen(file_path, "r"); + if (ff =3D=3D NULL) { + warn_report("Could not read the lid state."); + return; + } + + if (fgets(line, MAX_ALLOWED_LINE_LENGTH, ff) =3D=3D NULL) { + warn_report("Lid state unreadable."); + } else { + if (strstr(line, lid_state[LID_OPEN]) !=3D NULL) { + s->lid_state =3D LID_OPEN; + } else if (strstr(line, lid_state[LID_CLOSED]) !=3D NULL) { + s->lid_state =3D 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 =3D s->qmp_lid_open ? LID_OPEN : LID_CLOSED; + } else if (s->enable_procfs) { + button_get_lid_state(s); + } else { + s->lid_state =3D LID_CLOSED; + } +} + +static void button_probe_state(void *opaque) +{ + BUTTONState *s =3D opaque; + + uint8_t lid_state_before =3D s->lid_state; + + button_get_dynamic_status(s); + + if (lid_state_before !=3D s->lid_state) { + Object *obj =3D 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 =3D timer_new_ms(QEMU_CLOCK_VIRTUAL, + button_probe_state, s); + timer_mod(s->probe_state_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTU= AL) + + 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_FIL= E); +} + +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 =3D snprintf(lid_path, PATH_MAX, "%s/%s", path, LID_DIR); + if (path_len < 0 || path_len >=3D PATH_MAX) { + return false; + } + + dir =3D opendir(lid_path); + if (dir =3D=3D NULL) { + return false; + } + + ent =3D readdir(dir); + while (ent !=3D NULL) { + if (ent->d_name[0] !=3D '.') { + if (button_verify_lid_procfs(path, ent->d_name)) { + path_len =3D 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 =3D readdir(dir); + } + closedir(dir); + return false; +} + +static bool get_button_path(DeviceState *dev) +{ + BUTTONState *s =3D BUTTON_DEVICE(dev); + char procfs_path[PATH_MAX]; + int path_len; + + if (s->button_path) { + path_len =3D 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 =3D snprintf(procfs_path, sizeof(PROCFS_PATH), "%s", + PROCFS_PATH); + if (path_len < 0 || path_len >=3D 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 =3D ISA_DEVICE(dev); + BUTTONState *s =3D BUTTON_DEVICE(dev); + FWCfgState *fw_cfg =3D fw_cfg_find(); + uint16_t *button_port; + char err_details[32] =3D {}; + + 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 =3D true; + } + + if (s->probe_state_interval < MIN_BUTTON_PROBE_INTERVAL) { + error_setg(errp, "'probe_state_interval' must be greater than %d m= s", + MIN_BUTTON_PROBE_INTERVAL); + return; + } + + if (s->enable_procfs) { + if (!s->button_path) { + strcpy(err_details, " Try using 'procfs_path=3D'"); + } + + 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 =3D g_malloc(sizeof(*button_port)); + *button_port =3D 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[] =3D { + 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 =3D { + .name =3D "button", + .version_id =3D 1, + .minimum_version_id =3D 1, + .fields =3D (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 =3D BUTTON_DEVICE(adev); + + button_state =3D aml_local(0); + + sb_scope =3D aml_scope("\\_SB"); + dev =3D 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 =3D aml_field("LSTA", AML_BYTE_ACC, AML_NOLOCK, AML_PRESERVE); + aml_append(field, aml_named_field("LIDS", 8)); + aml_append(dev, field); + + method =3D 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 =3D 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 =3D DEVICE_CLASS(class); + AcpiDevAmlIfClass *adevc =3D ACPI_DEV_AML_IF_CLASS(class); + + dc->realize =3D button_realize; + device_class_set_props(dc, button_device_properties); + dc->vmsd =3D &button_vmstate; + adevc->build_dev_aml =3D build_button_aml; +} + +static uint64_t button_ioport_read(void *opaque, hwaddr addr, unsigned siz= e) +{ + BUTTONState *s =3D 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 =3D { + .read =3D button_ioport_read, + .impl =3D { + .min_access_size =3D 1, + .max_access_size =3D 1, + }, +}; + +static void button_instance_init(Object *obj) +{ + BUTTONState *s =3D BUTTON_DEVICE(obj); + + memory_region_init_io(&s->io, obj, &button_ops, s, "button", + BUTTON_LEN); +} + +static const TypeInfo button_info =3D { + .name =3D TYPE_BUTTON, + .parent =3D TYPE_ISA_DEVICE, + .instance_size =3D sizeof(BUTTONState), + .class_init =3D button_class_init, + .instance_init =3D button_instance_init, + .interfaces =3D (InterfaceInfo[]) { + { TYPE_ACPI_DEV_AML_IF }, + { }, + }, +}; + +static BUTTONState *find_button_device(void) +{ + Object *o =3D 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 =3D find_button_device(); + + if (!s) { + error_setg(errp, "No lid button device found"); + return; + } + + s->qmp_lid_open =3D open; + + Object *obj =3D 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 =3D find_button_device(); + LidButtonInfo *ret; + + if (!s) { + error_setg(errp, "No lid button device found"); + return NULL; + } + + ret =3D g_new0(LidButtonInfo, 1); + + if (s->use_qmp_control) { + ret->open =3D s->qmp_lid_open; + } else { + button_get_dynamic_status(s); + ret->open =3D (s->lid_state =3D=3D 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-bui= ld-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_de= v_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 =3D 64, ACPI_BATTERY_CHANGE_STATUS =3D 128, ACPI_AC_ADAPTER_CHANGE_STATUS =3D 1024, + ACPI_BUTTON_CHANGE_STATUS =3D 2048, } AcpiEventStatusBits; =20 #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 + * Marcel Apfelbaum + * Dmitry Fleytman + * + * 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' } --=20 2.51.0