Files
MontaukOS/kernel/src/ACPI/AcpiEvents.cpp
T

236 lines
9.6 KiB
C++

/*
* AcpiEvents.cpp
* ACPI fixed event handling (SCI interrupt, power button, etc.)
* Copyright (c) 2026 Daniel Hammer
*/
#include "AcpiEvents.hpp"
#include <ACPI/AcpiSleep.hpp>
#include <Io/IoPort.hpp>
#include <Hal/Apic/Apic.hpp>
#include <Hal/Apic/IoApic.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
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();
}
};
};