/* * AcpiEvents.cpp * ACPI fixed event handling (SCI interrupt, power button, etc.) * Copyright (c) 2026 Daniel Hammer */ #include "AcpiEvents.hpp" #include #include #include #include #include #include #include using namespace Kt; namespace Hal { namespace AcpiEvents { // ============================================================================ // State // ============================================================================ static uint32_t g_pm1aEventBlock = 0; static uint32_t g_pm1bEventBlock = 0; static uint8_t g_pm1EventLength = 0; static uint32_t g_gpe0Block = 0; static uint32_t g_gpe1Block = 0; static uint8_t g_gpe0Length = 0; static uint8_t g_gpe1Length = 0; static uint16_t g_sciIrq = 0; static bool g_initialized = false; // ============================================================================ // PM1 register helpers // ============================================================================ static uint16_t ReadPM1Status() { uint16_t sts = 0; if (g_pm1aEventBlock != 0) sts |= Io::In16((uint16_t)g_pm1aEventBlock); if (g_pm1bEventBlock != 0) sts |= Io::In16((uint16_t)g_pm1bEventBlock); return sts; } static uint16_t ReadPM1Enable() { uint16_t enableOffset = g_pm1EventLength / 2; uint16_t enable = 0; if (enableOffset == 0) return 0; if (g_pm1aEventBlock != 0) enable |= Io::In16((uint16_t)(g_pm1aEventBlock + enableOffset)); if (g_pm1bEventBlock != 0) enable |= Io::In16((uint16_t)(g_pm1bEventBlock + enableOffset)); return enable; } static void ClearPM1StatusBits(uint16_t bits) { // Write-1-to-clear semantics if (g_pm1aEventBlock != 0) Io::Out16(bits, (uint16_t)g_pm1aEventBlock); if (g_pm1bEventBlock != 0) Io::Out16(bits, (uint16_t)g_pm1bEventBlock); } static void SetPM1Events(uint16_t mask) { uint16_t enableOffset = g_pm1EventLength / 2; if (enableOffset == 0) return; if (g_pm1aEventBlock != 0) { Io::Out16(mask, (uint16_t)(g_pm1aEventBlock + enableOffset)); } if (g_pm1bEventBlock != 0) { Io::Out16(mask, (uint16_t)(g_pm1bEventBlock + enableOffset)); } } // The kernel does not currently evaluate GPE control methods. Leaving // firmware-enabled GPEs armed is therefore unsafe: an unacknowledged // level-triggered SCI can continuously retrigger and starve the BSP. static bool IsValidGpeIoBlock(uint32_t base, uint8_t length) { return base != 0 && length >= 2 && (length & 1) == 0 && base <= 0xFFFF && length <= 0x10000U - base; } static void DisableAndClearGpeBlock(uint32_t base, uint8_t length) { if (!IsValidGpeIoBlock(base, length)) return; uint16_t statusBytes = length / 2; for (uint16_t i = 0; i < statusBytes; i++) Io::Out8(0, (uint16_t)(base + statusBytes + i)); for (uint16_t i = 0; i < statusBytes; i++) Io::Out8(0xFF, (uint16_t)(base + i)); } static void QuiesceActiveGpeBlock(uint32_t base, uint8_t length) { if (!IsValidGpeIoBlock(base, length)) return; uint16_t statusBytes = length / 2; for (uint16_t i = 0; i < statusBytes; i++) { uint16_t statusPort = (uint16_t)(base + i); uint16_t enablePort = (uint16_t)(base + statusBytes + i); uint8_t enabled = Io::In8(enablePort); uint8_t active = Io::In8(statusPort) & enabled; if (active == 0) continue; // Disable the unsupported source before acknowledging it so a // level source cannot immediately assert the SCI again. Io::Out8(enabled & (uint8_t)~active, enablePort); Io::Out8(active, statusPort); } } static void DisableAndClearGpes() { DisableAndClearGpeBlock(g_gpe0Block, g_gpe0Length); DisableAndClearGpeBlock(g_gpe1Block, g_gpe1Length); } static void QuiesceActiveGpes() { QuiesceActiveGpeBlock(g_gpe0Block, g_gpe0Length); QuiesceActiveGpeBlock(g_gpe1Block, g_gpe1Length); } // ============================================================================ // SCI Interrupt Handler // ============================================================================ static void SciHandler(uint8_t /*irq*/, bool) { uint16_t sts = ReadPM1Status(); uint16_t activeFixedEvents = sts & ReadPM1Enable(); if (activeFixedEvents != 0) ClearPM1StatusBits(activeFixedEvents); // Firmware may re-enable a GPE after initialization. Since no GPE // AML methods are dispatched yet, disable and acknowledge every // active source rather than allowing an SCI interrupt storm. QuiesceActiveGpes(); // Note: HalIrqDispatch sends the LAPIC EOI before invoking handlers. } // ============================================================================ // Initialize // ============================================================================ void Initialize(ACPI::CommonSDTHeader* xsdt) { FADT::ParsedFADT fadt{}; if (!FADT::Parse(xsdt, fadt) || !fadt.Valid) { KernelLogStream(ERROR, "ACPI") << "Failed to parse FADT - ACPI events unavailable"; return; } g_pm1aEventBlock = fadt.PM1aEventBlock; g_pm1bEventBlock = fadt.PM1bEventBlock; g_pm1EventLength = fadt.PM1EventLength; g_gpe0Block = fadt.GPE0Block; g_gpe1Block = fadt.GPE1Block; g_gpe0Length = fadt.GPE0Length; g_gpe1Length = fadt.GPE1Length; g_sciIrq = fadt.SCI_Interrupt; if (g_pm1aEventBlock == 0) { KernelLogStream(ERROR, "ACPI") << "No PM1a event block - ACPI events unavailable"; return; } if (g_gpe0Length != 0 && !IsValidGpeIoBlock(g_gpe0Block, g_gpe0Length)) { KernelLogStream(WARNING, "ACPI") << "GPE0 block is not usable SystemIO (base=" << base::hex << (uint64_t)g_gpe0Block << ", length=" << base::dec << (uint64_t)g_gpe0Length << ")"; } if (g_gpe1Length != 0 && !IsValidGpeIoBlock(g_gpe1Block, g_gpe1Length)) { KernelLogStream(WARNING, "ACPI") << "GPE1 block is not usable SystemIO (base=" << base::hex << (uint64_t)g_gpe1Block << ", length=" << base::dec << (uint64_t)g_gpe1Length << ")"; } // Clear any pending status bits ClearPM1StatusBits( AcpiSleep::PM1_PWRBTN_STS | AcpiSleep::PM1_SLPBTN_STS | AcpiSleep::PM1_WAK_STS | AcpiSleep::PM1_TMR_STS | AcpiSleep::PM1_GBL_STS | AcpiSleep::PM1_RTC_STS | AcpiSleep::PM1_BM_STS); // Enable power button event SetPM1Events(AcpiSleep::PM1_PWRBTN_EN); // Disable unsupported firmware GPE sources before the level SCI is // routed. Clear stale status only after the enables are down. DisableAndClearGpes(); // Route SCI to an IRQ vector. The SCI is level-triggered, // active-low (ACPI spec requirement). The MADT may have an // Interrupt Source Override for this IRQ that already sets // these flags; RouteIrq applies overrides automatically. uint8_t sciVector = IRQ_VECTOR_BASE + (uint8_t)g_sciIrq; uint8_t bspApicId = (uint8_t)LocalApic::GetId(); // Register the handler before routing to avoid missing events RegisterIrqHandler((uint8_t)g_sciIrq, SciHandler); // Route via IoApic (applies MADT overrides for polarity/trigger) IoApic::RouteIrq((uint8_t)g_sciIrq, sciVector, bspApicId); g_initialized = true; KernelLogStream(OK, "ACPI") << "ACPI events initialized (SCI IRQ " << base::dec << (uint64_t)g_sciIrq << ", vector " << (uint64_t)sciVector << ")"; } void Reinitialize() { if (!g_initialized) return; // Resume may leave WAK/RTC/GBL bits pending. Because the SCI is // level-triggered, re-enabling interrupts without clearing them can // immediately retrigger the SCI in a tight loop. ClearPM1StatusBits( AcpiSleep::PM1_PWRBTN_STS | AcpiSleep::PM1_SLPBTN_STS | AcpiSleep::PM1_WAK_STS | AcpiSleep::PM1_TMR_STS | AcpiSleep::PM1_GBL_STS | AcpiSleep::PM1_RTC_STS | AcpiSleep::PM1_BM_STS); // Drop the suspend-time RTC wake enable and restore only the // fixed events we actually want during normal runtime. SetPM1Events(AcpiSleep::PM1_PWRBTN_EN); // Firmware and _WAK may have restored GPE enables while asleep. DisableAndClearGpes(); } }; };