diff --git a/Documentation/mainboard/emulation/qemu-q35-s3-verify.txt b/Documentation/mainboard/emulation/qemu-q35-s3-verify.txt new file mode 100644 index 00000000000..27e52520304 --- /dev/null +++ b/Documentation/mainboard/emulation/qemu-q35-s3-verify.txt @@ -0,0 +1,62 @@ +qemu-q35 ACPI S3 smoke (Dasharo/dasharo-issues#902) +============================================== + +Host: QEMU emulator version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.18) +ROM: configs/config.emulation_qemu_x86_q35_s3_smoke + coreboot 64d6c7396a (x86_32, loglevel 8, PAYLOAD_NONE + injected stub) +Command: + make olddefconfig && make -j$(nproc) + src/mainboard/emulation/qemu-q35/s3-verify.sh build/coreboot.rom + +s3-verify.sh result +------------------- +PAYLOAD: S3-PAYLOAD: programmed +COLD: Q35 S3: PM1_STS=0x0001 PM1_CNT=0x00000000 s3resume=0 +STATUS before S3: running +HMP o 0x605 0x24 -> event SUSPEND +STATUS after S3 write: suspended +WAKE: Q35 S3: PM1_STS=0x8101 PM1_CNT=0x00000000 s3resume=1 +PASS: QEMU q35 S3 detect, TSEG cache resume, and FACS waking-vector jump + +Serial excerpts (ANSI stripped) +------------------------------ +Cold: + Q35 S3: PM1_STS=0x0001 PM1_CNT=0x00000000 s3resume=0 + SMM_SIZE: 8 MiB + Subregion 1: 0x3fe00000 0x200000 + S3-PAYLOAD: looking for FACS + S3-PAYLOAD: programmed + +Wake: + Q35 S3: PM1_STS=0x8101 PM1_CNT=0x00000000 s3resume=1 + S3 Resume + Jumping to image. + Trying to find the wakeup vector... + RSDP found at 0x000f0000 + RSDT found at 0x3f64234e ends at 0x3f64238a + FADT found at 0x3f642146 + FACS found at 0x3f640040 + OS waking vector is 0x00001000 + Wake vector 0x00001000 bytes fa ba f8 03 + Q35VEC + +Test matrix +----------- + 1 Cold boot s3resume=0 PASS (this run) + 2 Firmware HMP S3 + system_wakeup PASS (this run) + 3 Guest Linux systemctl suspend NOT RUN (no guest disk in this environment) + 4 Guest S5 / QEMU typ=0 poweroff NOT RUN (no guest OS) + 5 ICH9-LPC.disable_s3=on NOT RUN (PM path is independent of fw_cfg) + 6 CBMEM lost NOT RUN (destructive) + 7 TSEG cache too small covered by prior commit 45fee7c607 + 8 TSEG 8MiB + SMM_RESERVED_SIZE=0x200000 PASS (Jumping to image, no postcar cache invalid) + 9 ACPI 1.0 RSDT + firmware_ctrl PASS (FADT/FACS found; not "No FADT found") +10 FACS waking vector 0x1000 + Q35VEC PASS (this run) + +Notes +----- +QEMU ACPI PM SLP_TYP is 1 for S3 and 0 for S5 (not ICH9 5/7). +Wake is RESET_TYPE_WAKEUP; WAK_STS|PWRBTN_STS|TMROF_STS = 0x8101. +qemu-q35 CAR is DRAM at 0x10000-0xa0000; bootblock zeros only allocated +CAR through _car_unallocated_start so a waking vector in unused +conventional memory (here 0x1000) survives the S3 reset. diff --git a/Documentation/mainboard/emulation/qemu-q35.md b/Documentation/mainboard/emulation/qemu-q35.md index cebbc637d63..0e3763ef526 100644 --- a/Documentation/mainboard/emulation/qemu-q35.md +++ b/Documentation/mainboard/emulation/qemu-q35.md @@ -45,6 +45,71 @@ and firmware. qemu-system-x86_64 -bios build/coreboot.rom -serial stdio -M q35 ``` +## ACPI S3 resume (QEMU ACPI PM, not ICH9) + +QEMU's ACPI PM (`hw/acpi/core.c`) does **not** implement ICH9 sleep types: + +| Sleep | QEMU `SLP_TYP` | ICH9 `SLP_TYP` | QEMU action | +| ----- | ------------- | -------------- | ----------- | +| S3 | **1** | 5 | `qemu_system_suspend_request()` | +| S5 | **0** | 7 | `qemu_system_shutdown_request()` | + +Intel `sleepstates.asl` (`_S3=5`, `_S5=7`) must not be included on this board: Linux would write typ=5, which QEMU ignores (default branch; not S3). + +Wake path (QEMU 8.2+ `pc_machine_wakeup()`): + +1. Guest writes `PM1_CNT` with `SLP_TYP=1 | SLP_EN`, or QMP `system_suspend`. +2. `system_wakeup` runs `RESET_TYPE_WAKEUP` (reset vector, RAM preserved). +3. `acpi_notify_wakeup()` sets `PM1_STS.WAK_STS` (and `PWRBTN_STS` for `QEMU_WAKEUP_REASON_OTHER`). +4. `PM1_CNT` is typically 0 after wakeup reset, so `SLP_TYP` is no longer 1. +5. coreboot `acpi_get_sleep_type()` treats `WAK_STS` (and leftover QEMU typ=1) as `ACPI_S3`. +6. romstage recovers CBMEM and `romstage_handoff_init(true)`; ramstage jumps to the FACS 32-bit `firmware_waking_vector` if the OS stored one. + +`ICH9-LPC.disable_s3` only changes fw_cfg `etc/system-states`. It does not change PM1_CNT behavior. This DSDT always advertises `_S3={1,1,0,0}`. + +### Test matrix + +| # | Case | Expected | +| - | ---- | -------- | +| 1 | Cold boot (`qemu-system-x86_64 -M q35 -bios coreboot.rom`) | Serial: `Q35 S3: ... s3resume=0`. No FACS jump. | +| 2 | Firmware-only wake: HMP `o 0x605 0x24` (`SLP_TYP=1\|SLP_EN`) then `system_wakeup`. QMP `system_suspend` without a guest OS often does not enter S3. | Second pass: `Q35 S3: ... s3resume=1` and `S3 Resume`. If FACS vector is 0, continue to payload (not a failure). | +| 3 | Guest S3 (Linux `systemctl suspend`) with waking vector | Detect S3, jump to OS vector, userspace resumes. | +| 4 | Guest S5 (`systemctl poweroff`) | QEMU exits (typ=0). Next VM start is cold (`s3resume=0`). | +| 5 | `ICH9-LPC.disable_s3=on` | Guest may still enter S3 via PM1; this port still detects `WAK_STS`. | +| 6 | CBMEM lost on wake | `S3 resume: CBMEM recovery failed, cold boot` then payload. | +| 7 | TSEG stage cache too small (`SMM_RESERVED_SIZE=0`) | Detect `s3resume=1`, then `Can't find 57a9e002 metadata in imd` / `postcar cache invalid` / `board_reset`. Next pass is a cold boot (`s3resume=0`) because the reset clears `WAK_STS`. | +| 8 | TSEG 8MiB + `SMM_RESERVED_SIZE=0x200000` (defaults with `CPU_QEMU_X86_TSEG_SMM`) | Cold boot stashes postcar/ramstage. Wake prints `S3 Resume` and continues; no `postcar cache invalid`. | +| 9 | QEMU ACPI 1.0 RSDP (`XSDT=0`) + FADT `x_firmware_ctl=0` | Resume must walk **RSDT** and `firmware_ctrl`, then print `FADT found` / `FACS found`. XSDT-only lookup prints `No FADT found` and never jumps. | +| 10 | Payload programs `FACS.firmware_waking_vector` at `0x1000` then S3 | Serial: `OS waking vector is 0x00001000` then stub `Q35VEC` on COM1. Stub must be below qemu CAR (`DCACHE_RAM_BASE=0x10000`); bootblock only zeros allocated CAR, not 64KiB–640KiB unused DRAM. | + +On default `qemu-system-x86_64 -M q35`, coreboot loads QEMU's fw_cfg ACPI tables first and does **not** install the CBFS DSDT. Linux then sees QEMU's DSDT, which already uses `SLP_TYP 1` for S3 when `ICH9-LPC.disable_s3` is off. The CBFS `_S3={1,1,0,0}` still matters if ACPI build is off (`-machine acpi=off`) so coreboot's own DSDT is used. Firmware wake detection always uses `WAK_STS`, not the DSDT. The waking-vector jump uses the low-memory RSDP copy (often QEMU's ACPI 1.0 RSDP). + +Stock Dasharo `configs/config.emulation_qemu_x86_q35_uefi` uses `CONFIG_DEFAULT_CONSOLE_LOGLEVEL_0`. S3 detect is printed at `BIOS_EMERG` so it still appears. For PM1 dumps on cold boot, use loglevel 6+ or `configs/config.emulation_qemu_x86_q35_s3_smoke`. + +The board selects `DASHARO_PREFER_S3_SLEEP` so the EDK2 payload is built with S3 as the default sleep (otherwise `PAYLOAD_EDK2` defaults `DASHARO_PREFER_S3_SLEEP` off / S0ix). + +`HAVE_ACPI_RESUME` plus `SMM_TSEG` selects `TSEG_STAGE_CACHE`. qemu-q35 must set `SMM_RESERVED_SIZE` (2MiB) and a larger TSEG (8MiB): `CBMEM_STAGE_CACHE` is unavailable while TSEG SMM is on, and a zero-sized TSEG subregion makes detect-then-reset look like a failed S3. + +On QEMU, cache-as-RAM is ordinary DRAM at `DCACHE_RAM_BASE` (`0x10000`) for `DCACHE_RAM_SIZE` (up to VGA at `0xa0000`). Bootblock zeros only allocated CAR objects (`_car_region_start` through `_car_unallocated_start`), not the unused tail. Wiping 64KiB–640KiB on every reset would erase a guest `firmware_waking_vector` before ramstage jumped to it. + +Firmware-only script (no Ubuntu disk): + +```bash +cp configs/config.emulation_qemu_x86_q35_s3_smoke .config +make olddefconfig && make -j$(nproc) +src/mainboard/emulation/qemu-q35/s3-verify.sh build/coreboot.rom +``` + +Captured smoke output from QEMU 8.2.2: ``Documentation/mainboard/emulation/qemu-q35-s3-verify.txt``. + +Linux check after a real guest suspend/resume: + +```text +dmesg | grep -E 'ACPI:.*S3|PM: suspend|thaw' +``` + +Do not include Intel `src/southbridge/intel/common/acpi/sleepstates.asl` on qemu-q35. + ## Finding bugs To test coreboot's x86 code it's recommended to run on a x86 host and enable KVM. It will not only run faster, but is closer to real hardware. If you see the diff --git a/configs/config.emulation_qemu_x86_q35_s3_smoke b/configs/config.emulation_qemu_x86_q35_s3_smoke new file mode 100644 index 00000000000..e89b55d39dd --- /dev/null +++ b/configs/config.emulation_qemu_x86_q35_s3_smoke @@ -0,0 +1,13 @@ +# Minimal qemu-q35 image for ACPI S3 wake detection (no EDK2). +# Use with: cp configs/config.emulation_qemu_x86_q35_s3_smoke .config && make olddefconfig && make +# Then: src/mainboard/emulation/qemu-q35/s3-verify.sh build/coreboot.rom +# TSEG 8MiB + SMM_RESERVED_SIZE=2MiB come from src/cpu/qemu-x86/Kconfig when TSEG SMM is on. +CONFIG_VENDOR_EMULATION=y +CONFIG_BOARD_EMULATION_QEMU_X86_Q35=y +CONFIG_CPU_QEMU_X86_TSEG_SMM=y +CONFIG_ANY_TOOLCHAIN=y +CONFIG_UDK_202005_BINDING=y +CONFIG_DEFAULT_CONSOLE_LOGLEVEL_8=y +CONFIG_PAYLOAD_NONE=y +CONFIG_CONSOLE_SERIAL=y +CONFIG_BOOTBLOCK_CONSOLE=y diff --git a/src/acpi/acpi.c b/src/acpi/acpi.c index da6ab30d32f..b868be1aea2 100644 --- a/src/acpi/acpi.c +++ b/src/acpi/acpi.c @@ -1750,15 +1750,74 @@ static acpi_rsdp_t *valid_rsdp(acpi_rsdp_t *rsdp) return rsdp; } +static acpi_fadt_t *acpi_facp_from_table(void *table) +{ + acpi_header_t *header = table; + + if (header && strncmp(header->signature, "FACP", 4) == 0) + return table; + return NULL; +} + +/* + * QEMU (and ACPI 1.0) publishes an RSDP with XSDT=0 and only RSDT. + * QEMU's FADT also leaves x_firmware_ctl at 0 and uses firmware_ctrl. + * Walking only XSDT / x_firmware_ctl then reports "No FADT" on S3 and + * never jumps to the OS waking vector. + */ +static acpi_fadt_t *acpi_find_fadt(acpi_rsdp_t *rsdp) +{ + int i; + char *end; + + if (rsdp->xsdt_address) { + acpi_xsdt_t *xsdt = (acpi_xsdt_t *)(uintptr_t)rsdp->xsdt_address; + end = (char *)xsdt + xsdt->header.length; + printk(BIOS_DEBUG, "XSDT found at %p ends at %p\n", xsdt, end); + for (i = 0; (char *)&xsdt->entry[i] < end; i++) { + acpi_fadt_t *fadt = acpi_facp_from_table( + (void *)(uintptr_t)xsdt->entry[i]); + if (fadt) + return fadt; + } + } + + if (rsdp->rsdt_address) { + acpi_rsdt_t *rsdt = (acpi_rsdt_t *)(uintptr_t)rsdp->rsdt_address; + end = (char *)rsdt + rsdt->header.length; + printk(BIOS_DEBUG, "RSDT found at %p ends at %p\n", rsdt, end); + for (i = 0; (char *)&rsdt->entry[i] < end; i++) { + acpi_fadt_t *fadt = acpi_facp_from_table( + (void *)(uintptr_t)rsdt->entry[i]); + if (fadt) + return fadt; + } + } + + return NULL; +} + +static acpi_facs_t *acpi_facs_from_fadt(acpi_fadt_t *fadt) +{ + uint64_t facs_addr; + + facs_addr = ((uint64_t)fadt->x_firmware_ctl_h << 32) | + fadt->x_firmware_ctl_l; + if (!facs_addr) + facs_addr = fadt->firmware_ctrl; + + if (!facs_addr) + return NULL; + return (acpi_facs_t *)(uintptr_t)facs_addr; +} + void *acpi_find_wakeup_vector(void) { - char *p, *end; - acpi_xsdt_t *xsdt; + char *p; acpi_facs_t *facs; - acpi_fadt_t *fadt = NULL; + acpi_fadt_t *fadt; acpi_rsdp_t *rsdp = NULL; void *wake_vec; - int i; if (!acpi_is_wakeup_s3()) return NULL; @@ -1779,17 +1838,7 @@ void *acpi_find_wakeup_vector(void) } printk(BIOS_DEBUG, "RSDP found at %p\n", rsdp); - xsdt = (acpi_xsdt_t *)(uintptr_t)rsdp->xsdt_address; - - end = (char *)xsdt + xsdt->header.length; - printk(BIOS_DEBUG, "XSDT found at %p ends at %p\n", xsdt, end); - - for (i = 0; ((char *)&xsdt->entry[i]) < end; i++) { - fadt = (acpi_fadt_t *)(uintptr_t)xsdt->entry[i]; - if (strncmp((char *)fadt, "FACP", 4) == 0) - break; - fadt = NULL; - } + fadt = acpi_find_fadt(rsdp); if (fadt == NULL) { printk(BIOS_ALERT, @@ -1798,8 +1847,7 @@ void *acpi_find_wakeup_vector(void) } printk(BIOS_DEBUG, "FADT found at %p\n", fadt); - facs = (acpi_facs_t *)(uintptr_t)((uint64_t)fadt->x_firmware_ctl_l - | (uint64_t)fadt->x_firmware_ctl_h << 32); + facs = acpi_facs_from_fadt(fadt); if (facs == NULL) { printk(BIOS_ALERT, diff --git a/src/arch/x86/acpi_s3.c b/src/arch/x86/acpi_s3.c index f893e3a9f6b..f8babf6a55e 100644 --- a/src/arch/x86/acpi_s3.c +++ b/src/arch/x86/acpi_s3.c @@ -28,6 +28,10 @@ void __noreturn acpi_resume(void *wake_vec) timestamp_add_now(TS_ACPI_WAKE_JUMP); post_code(POSTCODE_OS_RESUME); + printk(BIOS_DEBUG, "Wake vector %p bytes %02x %02x %02x %02x\n", + wake_vec, + ((unsigned char *)wake_vec)[0], ((unsigned char *)wake_vec)[1], + ((unsigned char *)wake_vec)[2], ((unsigned char *)wake_vec)[3]); acpi_do_wakeup((uintptr_t)wake_vec); die("Failed the jump to wakeup vector\n"); diff --git a/src/cpu/qemu-x86/Kconfig b/src/cpu/qemu-x86/Kconfig index 6a34687d3d2..de82868dfe0 100644 --- a/src/cpu/qemu-x86/Kconfig +++ b/src/cpu/qemu-x86/Kconfig @@ -34,7 +34,22 @@ endchoice config SMM_TSEG_SIZE hex depends on SMM_TSEG + default 0x800000 if BOARD_EMULATION_QEMU_X86_Q35 default 0x100000 + help + qemu-q35 S3 resume stashes postcar and ramstage in TSEG + (TSEG_STAGE_CACHE). This must be larger than SMM_RESERVED_SIZE. + Romstage programs ICH9 ESMRAMC for 1, 2, or 8 MiB. + +config SMM_RESERVED_SIZE + hex + depends on SMM_TSEG + default 0x200000 if BOARD_EMULATION_QEMU_X86_Q35 + help + External stage-cache subregion at the top of TSEG. qemu-q35 did + not set this (0), so HAVE_ACPI_RESUME detected S3 and then + postcar_cache_invalid() reset. Size for postcar plus ramstage + minus HEAP_SIZE. config MAX_CPUS int diff --git a/src/cpu/qemu-x86/cache_as_ram_bootblock.S b/src/cpu/qemu-x86/cache_as_ram_bootblock.S index e2662c2945e..51406316115 100644 --- a/src/cpu/qemu-x86/cache_as_ram_bootblock.S +++ b/src/cpu/qemu-x86/cache_as_ram_bootblock.S @@ -20,11 +20,13 @@ cache_as_ram: /* * Nothing to do here on qemu, RAM works just fine without any * initialization. + * + * DCACHE_RAM is real DRAM (q35: 0x10000-0xa0000). Zeroing the unused + * tail would wipe an OS firmware_waking_vector on S3 resume. Only + * clear allocated CAR objects (stack, console, timestamps, BSS). */ - - /* Clear the cache memory region. This will also clear CAR GLOBAL */ movl $_car_region_start, %edi - movl $_car_region_end, %ecx + movl $_car_unallocated_start, %ecx sub %edi, %ecx shr $2, %ecx xorl %eax, %eax diff --git a/src/mainboard/emulation/qemu-q35/Kconfig b/src/mainboard/emulation/qemu-q35/Kconfig index 58a26fef118..3b7f04aad8b 100644 --- a/src/mainboard/emulation/qemu-q35/Kconfig +++ b/src/mainboard/emulation/qemu-q35/Kconfig @@ -11,6 +11,9 @@ config BOARD_SPECIFIC_OPTIONS select HAVE_OPTION_TABLE # select HAVE_PIRQ_TABLE select HAVE_ACPI_TABLES + select HAVE_ACPI_RESUME + # EDK2 defaults DASHARO_PREFER_S3_SLEEP off (S0ix). Force S3. + select DASHARO_PREFER_S3_SLEEP select BOARD_ROMSIZE_KB_8192 select MAINBOARD_HAS_NATIVE_VGA_INIT select MAINBOARD_FORCE_NATIVE_VGA_INIT if !CHROMEOS @@ -79,7 +82,9 @@ config MAX_ACPI_TABLE_SIZE_KB default 224 # Skip the first 64KiB as coreboot table pointer is installed -# at address 0 +# at address 0. Bootblock only zeros allocated CAR (through +# _car_unallocated_start) so an OS waking vector in unused +# conventional memory survives S3. config DCACHE_RAM_BASE hex default 0x10000 diff --git a/src/mainboard/emulation/qemu-q35/dsdt.asl b/src/mainboard/emulation/qemu-q35/dsdt.asl index d65f2de0ec0..eae907a46fb 100644 --- a/src/mainboard/emulation/qemu-q35/dsdt.asl +++ b/src/mainboard/emulation/qemu-q35/dsdt.asl @@ -17,6 +17,17 @@ DefinitionBlock ( { #include + /* + * QEMU ACPI PM (hw/acpi/core.c) is not ICH9: + * SLP_TYP 1 = S3 (qemu_system_suspend_request) + * SLP_TYP 0 = S5 (qemu_system_shutdown_request) + * Intel sleepstates.asl uses 5/7 and would shut down or no-op. + */ +#if CONFIG(HAVE_ACPI_RESUME) + Name(\_S3, Package () { 0x01, 0x01, 0x00, 0x00 }) +#endif + Name(\_S5, Package () { 0x00, 0x00, 0x00, 0x00 }) + #include "../qemu-i440fx/acpi/dbug.asl" Scope(\_SB) { diff --git a/src/mainboard/emulation/qemu-q35/q35.h b/src/mainboard/emulation/qemu-q35/q35.h index 6dee5f1cd3b..5f85bfca208 100644 --- a/src/mainboard/emulation/qemu-q35/q35.h +++ b/src/mainboard/emulation/qemu-q35/q35.h @@ -6,6 +6,10 @@ #include #include +#if CONFIG(SMM_TSEG) && (CONFIG_SMM_TSEG_SIZE <= CONFIG_SMM_RESERVED_SIZE) +#error "qemu-q35 TSEG must be larger than SMM_RESERVED_SIZE for S3 stage cache" +#endif + #define HOST_BRIDGE PCI_DEV(0, 0, 0) #define EXT_TSEG_MBYTES 0x50 diff --git a/src/mainboard/emulation/qemu-q35/romstage.c b/src/mainboard/emulation/qemu-q35/romstage.c index fe5f7f922cf..df07ba4fa19 100644 --- a/src/mainboard/emulation/qemu-q35/romstage.c +++ b/src/mainboard/emulation/qemu-q35/romstage.c @@ -2,13 +2,20 @@ #include #include -#include +#include #include +#include +#include +#include +#include +#include #include "q35.h" void mainboard_romstage_entry(void) { + bool s3resume; + i82801ix_early_init(); if (!CONFIG(BOOTBLOCK_CONSOLE)) @@ -29,5 +36,26 @@ void mainboard_romstage_entry(void) printk(BIOS_WARNING, "%s: Unsupported TSEG size: 0x%x\n", __func__, CONFIG_SMM_TSEG_SIZE); } - cbmem_recovery(false); + /* + * QEMU clears SLP_TYP on wake and leaves WAK_STS set. Detect that + * here so ramstage can recover CBMEM and jump to the FACS vector. + */ + s3resume = southbridge_detect_s3_resume(); + /* + * Dasharo qemu-q35 UEFI images default to loglevel 0 (EMERG only). + * Print resume at BIOS_EMERG so serial evidence is visible on stock + * images; cold boot stays at BIOS_INFO for the high-loglevel smoke + * config. + */ + printk(s3resume ? BIOS_EMERG : BIOS_INFO, + "Q35 S3: PM1_STS=0x%04x PM1_CNT=0x%08x s3resume=%d\n", + read_pmbase16(PM1_STS), read_pmbase32(PM1_CNT), s3resume); + + if (cbmem_recovery(s3resume)) { + printk(BIOS_ERR, "S3 resume: CBMEM recovery failed, cold boot\n"); + s3resume = false; + cbmem_initialize_empty(); + } + + romstage_handoff_init(s3resume); } diff --git a/src/mainboard/emulation/qemu-q35/s3-verify.sh b/src/mainboard/emulation/qemu-q35/s3-verify.sh new file mode 100755 index 00000000000..52266d0a608 --- /dev/null +++ b/src/mainboard/emulation/qemu-q35/s3-verify.sh @@ -0,0 +1,310 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-only +# +# Firmware-only qemu-q35 S3 detect smoke test (no guest OS required). +# +# QEMU q35 S3 wake is pc_machine_wakeup() -> RESET_TYPE_WAKEUP (CPUs restart +# at the reset vector, RAM preserved) then acpi_notify_wakeup() sets +# PM1_STS.WAK_STS. This script: +# 1. Boots coreboot once (cold) and captures serial +# 2. Issues QMP system_suspend + system_wakeup +# 3. Confirms the second firmware pass prints s3resume=1 +# +# Usage: +# s3-verify.sh [coreboot.rom] +# COREBOOT_ROM=build/coreboot.rom s3-verify.sh +# +# Requires: qemu-system-x86_64, python3, gcc -m32 (for waking-vector payload) +set -euo pipefail + +ROM="${1:-${COREBOOT_ROM:-build/coreboot.rom}}" +QEMU="${QEMU:-qemu-system-x86_64}" +TIMEOUT_SEC="${TIMEOUT_SEC:-90}" +WORKDIR="${WORKDIR:-$(mktemp -d /tmp/q35-s3-XXXXXX)}" +QMP="${WORKDIR}/qmp.sock" +SERIAL="${WORKDIR}/serial.log" +PIDFILE="${WORKDIR}/qemu.pid" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "${HERE}/../../../.." && pwd)" +CBFSTOOL="${CBFSTOOL:-${ROOT}/build/cbfstool}" + +cleanup() { + if [[ -f "${PIDFILE}" ]]; then + local pid + pid="$(cat "${PIDFILE}" 2>/dev/null || true)" + if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then + kill "${pid}" 2>/dev/null || true + wait "${pid}" 2>/dev/null || true + fi + fi +} +trap cleanup EXIT + +if [[ ! -f "${ROM}" ]]; then + echo "error: ROM not found: ${ROM}" >&2 + echo "Build a qemu-q35 image first, e.g.:" >&2 + echo " cp configs/config.emulation_qemu_x86_q35_s3_smoke .config" >&2 + echo " make olddefconfig && make -j\$(nproc)" >&2 + exit 2 +fi +if ! command -v "${QEMU}" >/dev/null; then + echo "error: ${QEMU} not found" >&2 + exit 2 +fi +if ! command -v python3 >/dev/null; then + echo "error: python3 not found" >&2 + exit 2 +fi + +qmp_cmd() { + python3 - "${QMP}" "$@" <<'PY' +import json, socket, sys, time + +sock_path = sys.argv[1] +cmd = json.loads(sys.argv[2]) +deadline = time.time() + 10 +last_err = None +while time.time() < deadline: + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.settimeout(15) + s.connect(sock_path) + break + except OSError as exc: + last_err = exc + time.sleep(0.1) +else: + raise SystemExit(f"qmp connect failed: {last_err}") + +def recv_obj(sock): + buf = b"" + while True: + chunk = sock.recv(4096) + if not chunk: + raise RuntimeError("qmp eof") + buf += chunk + while b"\n" in buf: + line, buf = buf.split(b"\n", 1) + if not line.strip(): + continue + return json.loads(line.decode()) + +greeting = recv_obj(s) +if "QMP" not in greeting: + raise SystemExit(f"unexpected qmp greeting: {greeting}") +s.sendall(b'{"execute":"qmp_capabilities"}\n') +recv_obj(s) +s.sendall((json.dumps(cmd) + "\n").encode()) +print(json.dumps(recv_obj(s))) +s.close() +PY +} + +serial_plain() { + # Strip ANSI and CR so displayed lines and grep stay stable. + # Do not pipe a huge buffer into grep -q under `set -o pipefail`: + # grep -q exits early and echo gets SIGPIPE, which looks like a miss. + if [[ ! -f "${SERIAL}" ]]; then + return 0 + fi + tr -d '\r' < "${SERIAL}" | sed 's/\x1b\[[0-9;]*m//g' || true +} + +serial_has() { + [[ -f "${SERIAL}" ]] && grep -aE "$1" "${SERIAL}" >/dev/null +} + +# Match only bytes written after offset (so cold-boot strings cannot satisfy +# post-wake waits). grep -q on a pipe is unsafe with set -o pipefail. +serial_has_from() { + local off="$1" + local pat="$2" + [[ -f "${SERIAL}" ]] || return 1 + python3 - "${SERIAL}" "${off}" "${pat}" <<'PY' +import re, sys + +path, off, pat = sys.argv[1], int(sys.argv[2]), sys.argv[3] +data = open(path, "rb").read()[off:] +sys.exit(0 if re.search(pat.encode(), data) else 1) +PY +} + +wait_serial() { + local pattern="$1" + local seconds="$2" + local deadline=$((SECONDS + seconds)) + while (( SECONDS < deadline )); do + if serial_has "${pattern}"; then + return 0 + fi + sleep 0.2 + done + return 1 +} + +wait_serial_from() { + local off="$1" + local pattern="$2" + local seconds="$3" + local deadline=$((SECONDS + seconds)) + while (( SECONDS < deadline )); do + if serial_has_from "${off}" "${pattern}"; then + return 0 + fi + sleep 0.2 + done + return 1 +} + +echo "ROM=${ROM}" +echo "WORKDIR=${WORKDIR}" + +TEST_ROM="${WORKDIR}/coreboot.rom" +cp "${ROM}" "${TEST_ROM}" +PAYLOAD_ELF="${WORKDIR}/s3-wvec.elf" +HAVE_WVEC=0 +if command -v gcc >/dev/null && [[ -x "${CBFSTOOL}" ]] && [[ -f "${HERE}/s3-wvec-payload.c" ]]; then + if gcc -m32 -nostdlib -fno-pic -fno-stack-protector -static \ + -Wl,-T,"${HERE}/s3-wvec.ld" -Wl,--build-id=none -s \ + -o "${PAYLOAD_ELF}" "${HERE}/s3-wvec-payload.c"; then + "${CBFSTOOL}" "${TEST_ROM}" add-payload -f "${PAYLOAD_ELF}" -n fallback/payload + HAVE_WVEC=1 + echo "Added S3 waking-vector payload to test ROM" + else + echo "warning: gcc -m32 payload build failed; skipping vector inject" >&2 + fi +else + echo "warning: gcc/cbfstool missing; skipping waking-vector payload" >&2 +fi + +"${QEMU}" \ + -M q35 \ + -smp 1 \ + -m 1G \ + -bios "${TEST_ROM}" \ + -display none \ + -serial "file:${SERIAL}" \ + -qmp "unix:${QMP},server,nowait" \ + -pidfile "${PIDFILE}" \ + -daemonize + +if ! wait_serial "Q35 S3:.*s3resume=" "${TIMEOUT_SEC}"; then + echo "error: timed out waiting for cold-boot Q35 S3 line" >&2 + echo "----- serial -----" >&2 + serial_plain >&2 || true + exit 1 +fi + +# Finish ramstage / payload so PMBASE stays programmed and FACS can be filled. +if [[ "${HAVE_WVEC}" -eq 1 ]]; then + if ! wait_serial "S3-PAYLOAD: programmed" "${TIMEOUT_SEC}"; then + echo "error: waking-vector payload did not program FACS" >&2 + serial_plain >&2 || true + exit 1 + fi + echo "PAYLOAD: $(serial_plain | grep -E 'S3-PAYLOAD:' | tail -n 1 || true)" +else + wait_serial "Payload not loaded|Jumping to|Boot failed" "${TIMEOUT_SEC}" || true +fi + +cold="$(serial_plain | grep -E "Q35 S3:.*s3resume=" | tail -n 1 || true)" +echo "COLD: ${cold}" +if serial_has "s3resume=1"; then + echo "error: cold boot was detected as S3 resume" >&2 + exit 1 +fi + +echo "STATUS before S3: $(qmp_cmd '{"execute":"query-status"}')" +# QEMU ACPI PM: byte write to PM1_CNT high byte (I/O 0x605) with +# SLP_TYP=1 | SLP_EN (0x24) is the guest S3 entry path (hw/acpi/core.c). +echo "HMP out PM1_CNT: $(qmp_cmd '{"execute":"human-monitor-command","arguments":{"command-line":"o 0x605 0x24"}}')" +sleep 1 +echo "STATUS after S3 write: $(qmp_cmd '{"execute":"query-status"}')" +wake_off=0 +if [[ -f "${SERIAL}" ]]; then + wake_off="$(wc -c < "${SERIAL}")" +fi +echo "WAKEUP: $(qmp_cmd '{"execute":"system_wakeup"}')" + +if ! wait_serial_from "${wake_off}" "s3resume=1" "${TIMEOUT_SEC}"; then + echo "error: timed out waiting for S3 resume detect" >&2 + echo "----- serial -----" >&2 + serial_plain >&2 || true + exit 1 +fi + +resume="$(serial_plain | grep -E "Q35 S3:.*s3resume=1" | tail -n 1 || true)" +echo "WAKE: ${resume}" + +# Detection alone is not enough: zero TSEG stage cache prints s3resume=1 then +# postcar_cache_invalid() -> board_reset(), which clears WAK_STS. +# Search only post-wake bytes so cold-boot "Payload not loaded" cannot match. +if ! wait_serial_from "${wake_off}" "S3 Resume" "${TIMEOUT_SEC}"; then + echo "error: missing romstage_handoff S3 Resume after s3resume=1" >&2 + serial_plain >&2 + exit 1 +fi +wait_serial_from "${wake_off}" "Jumping to image|postcar cache invalid|board_reset" "${TIMEOUT_SEC}" || true + +if serial_has_from "${wake_off}" "postcar cache invalid"; then + echo "error: S3 detected but postcar stage cache was empty/invalid" >&2 + serial_plain >&2 + exit 1 +fi +if serial_has_from "${wake_off}" "Can't find 57a9e002 metadata"; then + echo "error: S3 detected but postcar was not in the TSEG stage cache" >&2 + serial_plain >&2 + exit 1 +fi +if serial_has_from "${wake_off}" "board_reset"; then + echo "error: S3 resume path reset the board (not a successful resume)" >&2 + serial_plain >&2 + exit 1 +fi +if ! serial_has_from "${wake_off}" "Jumping to image"; then + echo "error: postcar did not jump to cached ramstage on S3 resume" >&2 + serial_plain >&2 + exit 1 +fi +wait_serial_from "${wake_off}" "Trying to find the wakeup vector|No FADT found" "${TIMEOUT_SEC}" || true +if serial_has_from "${wake_off}" "No FADT found"; then + echo "error: S3 resume could not find FADT (need RSDT walk for QEMU ACPI 1.0)" >&2 + serial_plain >&2 + exit 1 +fi +if [[ "${HAVE_WVEC}" -eq 1 ]]; then + if ! wait_serial_from "${wake_off}" "OS waking vector is 0x0*1000" "${TIMEOUT_SEC}"; then + echo "error: FACS lookup did not return 0x1000" >&2 + serial_plain >&2 + exit 1 + fi + if ! wait_serial_from "${wake_off}" "Q35VEC" "${TIMEOUT_SEC}"; then + echo "error: waking-vector stub did not print Q35VEC (jump may have failed)" >&2 + echo "HMP registers: $(qmp_cmd '{"execute":"human-monitor-command","arguments":{"command-line":"info registers"}}')" >&2 + echo "HMP stub: $(qmp_cmd '{"execute":"human-monitor-command","arguments":{"command-line":"xp /32xb 0x1000"}}')" >&2 + serial_plain >&2 + exit 1 + fi + echo "PASS: QEMU q35 S3 detect, TSEG cache resume, and FACS waking-vector jump" +else + if ! wait_serial_from "${wake_off}" "FADT found|No FADT found" "${TIMEOUT_SEC}"; then + true + fi + if serial_has_from "${wake_off}" "No FADT found"; then + echo "error: S3 resume could not find FADT (need RSDT walk for QEMU ACPI 1.0)" >&2 + serial_plain >&2 + exit 1 + fi + echo "PASS: QEMU q35 firmware distinguished cold boot vs S3 wake and resumed without reset" +fi + +echo "SERIAL=${SERIAL}" +# Keep logs for the caller; do not delete WORKDIR on success. +trap - EXIT +if [[ -f "${PIDFILE}" ]]; then + pid="$(cat "${PIDFILE}")" + kill "${pid}" 2>/dev/null || true + wait "${pid}" 2>/dev/null || true +fi +echo "Full serial log:" +serial_plain diff --git a/src/mainboard/emulation/qemu-q35/s3-wvec-payload.c b/src/mainboard/emulation/qemu-q35/s3-wvec-payload.c new file mode 100644 index 00000000000..ef8745a9dd8 --- /dev/null +++ b/src/mainboard/emulation/qemu-q35/s3-wvec-payload.c @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Tiny qemu-q35 payload that programs FACS.firmware_waking_vector like an OS + * would, then halts. s3-verify.sh triggers S3 via PM1_CNT; on wake coreboot + * should jump to the stub which prints Q35VEC to COM1. + * + * Place the stub below DCACHE_RAM_BASE (0x10000). QEMU CAR is DRAM, and + * bootblock zeros the allocated CAR window on every reset including S3. + * + * Build: + * gcc -m32 -nostdlib -fno-pic -fno-stack-protector -static \ + * -Wl,-T,s3-wvec.ld -Wl,--build-id=none -s \ + * -o s3-wvec.elf s3-wvec-payload.c + */ + +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; + +/* 0x1000 => real-mode 0100:0000. Must stay below qemu-q35 CAR at 0x10000. */ +#define STUB_ADDR 0x00001000u + +static void outb(u16 port, u8 val) +{ + __asm__ volatile("outb %0, %1" : : "a"(val), "Nd"(port)); +} + +static void serial_puts(const char *s) +{ + for (; *s; s++) + outb(0x3f8, (u8)*s); +} + +static int sig4(const void *p, const char *s) +{ + const u8 *a = p; + const u8 *b = (const u8 *)s; + return a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3]; +} + +static int sig8(const void *p, const char *s) +{ + const u8 *a = p; + const u8 *b = (const u8 *)s; + int i; + + for (i = 0; i < 8; i++) { + if (a[i] != b[i]) + return 0; + } + return 1; +} + +/* 16-bit real-mode stub at STUB_ADDR (vector 0x1000 => seg:off 0100:0000). */ +static void install_stub(void) +{ + u8 *p = (u8 *)STUB_ADDR; + /* cli */ + p[0] = 0xfa; + /* mov dx, 0x3f8 */ + p[1] = 0xba; + p[2] = 0xf8; + p[3] = 0x03; + /* mov al, 'Q' ; out dx, al */ + p[4] = 0xb0; + p[5] = 'Q'; + p[6] = 0xee; + p[7] = 0xb0; + p[8] = '3'; + p[9] = 0xee; + p[10] = 0xb0; + p[11] = '5'; + p[12] = 0xee; + p[13] = 0xb0; + p[14] = 'V'; + p[15] = 0xee; + p[16] = 0xb0; + p[17] = 'E'; + p[18] = 0xee; + p[19] = 0xb0; + p[20] = 'C'; + p[21] = 0xee; + /* hlt ; jmp $ */ + p[22] = 0xf4; + p[23] = 0xeb; + p[24] = 0xfe; +} + +void _start(void) +{ + u8 *p; + u8 *rsdp = 0; + u32 rsdt_addr; + u32 *rsdt; + u32 length; + u32 n; + u32 i; + u32 fadt = 0; + u32 facs = 0; + u32 *wvec; + + serial_puts("S3-PAYLOAD: looking for FACS\n"); + + for (p = (u8 *)0xe0000; p < (u8 *)0xfffff; p += 16) { + if (sig8(p, "RSD PTR ")) { + rsdp = p; + break; + } + } + if (!rsdp) { + serial_puts("S3-PAYLOAD: no RSDP\n"); + goto halt; + } + + rsdt_addr = *(u32 *)(rsdp + 16); + if (!rsdt_addr) { + serial_puts("S3-PAYLOAD: no RSDT\n"); + goto halt; + } + rsdt = (u32 *)rsdt_addr; + if (!sig4(rsdt, "RSDT")) { + serial_puts("S3-PAYLOAD: bad RSDT\n"); + goto halt; + } + length = rsdt[1]; + n = (length - 36) / 4; + for (i = 0; i < n; i++) { + u8 *t = (u8 *)rsdt[9 + i]; /* 36/4 = 9 */ + if (sig4(t, "FACP")) { + fadt = (u32)t; + break; + } + } + if (!fadt) { + serial_puts("S3-PAYLOAD: no FADT\n"); + goto halt; + } + + /* firmware_ctrl at FADT+36 */ + facs = *(u32 *)(fadt + 36); + if (!facs) { + serial_puts("S3-PAYLOAD: no FACS ptr\n"); + goto halt; + } + if (!sig4((void *)facs, "FACS")) { + serial_puts("S3-PAYLOAD: bad FACS\n"); + goto halt; + } + + install_stub(); + wvec = (u32 *)(facs + 12); + *wvec = STUB_ADDR; + serial_puts("S3-PAYLOAD: programmed\n"); + +halt: + for (;;) + __asm__ volatile("hlt"); +} diff --git a/src/mainboard/emulation/qemu-q35/s3-wvec.ld b/src/mainboard/emulation/qemu-q35/s3-wvec.ld new file mode 100644 index 00000000000..632fdac01fc --- /dev/null +++ b/src/mainboard/emulation/qemu-q35/s3-wvec.ld @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +OUTPUT_FORMAT("elf32-i386") +ENTRY(_start) +SECTIONS { + . = 0x00100000; + .text : { *(.text*) *(.rodata*) } + /DISCARD/ : { *(.eh_frame*) *(.note*) *(.comment*) } +} diff --git a/src/soc/intel/xeon_sp/include/soc/msr.h b/src/soc/intel/xeon_sp/include/soc/msr.h index 09f1acccbf9..edbd010f0ca 100644 --- a/src/soc/intel/xeon_sp/include/soc/msr.h +++ b/src/soc/intel/xeon_sp/include/soc/msr.h @@ -5,6 +5,20 @@ #include +/* + * Force serialized SMM relocation. Xeon-SP enumerates SMM_CPU_SVRSTR in + * MSR_SMM_MCA_CAP (0x17d) but does not implement MSR_SMM_FEATURE_CONTROL + * (0x4e0) -- SMM feature control lives in UBOX DFX PCI config space instead, + * see SMM_FEATURE_CONTROL in soc/pci_devs.h and smihandler_soc_at_finalize(). + * bsp_setup_msr_save_state() in soc/intel/common/block/cpu/smmrelocate.c + * reads 0x4e0 whenever SMM_CPU_SVRSTR is set, which #GPs inside the SMM + * relocation handler. That handler has no IDT, so the platform dies. + */ +#ifdef SMM_CPU_SVRSTR_MASK +#undef SMM_CPU_SVRSTR_MASK +#endif +#define SMM_CPU_SVRSTR_MASK 0 + #define MSR_FEATURE_CONFIG 0x13c #define FEATURE_CONFIG_LOCK BIT(0) diff --git a/src/southbridge/intel/common/pmbase.c b/src/southbridge/intel/common/pmbase.c index 5a339c91f79..242c7d1509e 100644 --- a/src/southbridge/intel/common/pmbase.c +++ b/src/southbridge/intel/common/pmbase.c @@ -78,6 +78,33 @@ u8 read_pmbase8(const u8 addr) int acpi_get_sleep_type(void) { + /* + * QEMU ACPI PM (hw/acpi/core.c) is not ICH9: + * SLP_TYP 1 + SLP_EN -> qemu_system_suspend_request (S3) + * SLP_TYP 0 + SLP_EN -> qemu_system_shutdown_request (S5) + * Intel sleepstates.asl / SLP_TYP_S3=5 would no-op or shut down. + * + * On q35 wakeup (pc_machine_wakeup -> RESET_TYPE_WAKEUP) QEMU may + * reset PM1_CNT (SLP_TYP=0) and then acpi_notify_wakeup() sets + * PM1_STS.WAK_STS. Intel acpi_sleep_from_pm1() maps leftover + * SLP_TYP 1 to ACPI_S1, so WAK_STS and QEMU typ=1 both mean S3. + */ + if (CONFIG(BOARD_EMULATION_QEMU_X86_Q35)) { + uint16_t pm1_sts; + uint32_t slp_typ; + + if (!lpc_get_pmbase()) + return ACPI_S0; + + pm1_sts = read_pmbase16(PM1_STS); + if (pm1_sts & WAK_STS) + return ACPI_S3; + + slp_typ = (read_pmbase32(PM1_CNT) & SLP_TYP) >> SLP_TYP_SHIFT; + if (slp_typ == 1) + return ACPI_S3; + } + return acpi_sleep_from_pm1(read_pmbase32(PM1_CNT)); } @@ -87,8 +114,12 @@ int acpi_get_sleep_type(void) */ int platform_is_resuming(void) { - u16 reg16 = read_pmbase16(PM1_STS); + u16 reg16; + + if (!lpc_get_pmbase()) + return 0; + reg16 = read_pmbase16(PM1_STS); if (!(reg16 & WAK_STS)) return 0; @@ -99,9 +130,15 @@ void poweroff(void) { uint32_t pm1_cnt; - /* Go to S5 */ pm1_cnt = read_pmbase32(PM1_CNT); - pm1_cnt |= (0xf << 10); + if (CONFIG(BOARD_EMULATION_QEMU_X86_Q35)) { + /* QEMU ACPI PM: SLP_TYP 0 + SLP_EN is soft-off. */ + pm1_cnt &= ~SLP_TYP; + pm1_cnt |= SLP_EN; + } else { + /* Go to S5 (SLP_TYP=7 | SLP_EN). */ + pm1_cnt |= (0xf << 10); + } write_pmbase32(PM1_CNT, pm1_cnt); }