SchOS/kernel/arch/x86_64/ioapic.c
Nexoniarz 93f981c7ea acpi: uACPI, for interrupt routing, the power button and power-off
The firmware's AML is now interpreted. uACPI 6.1.1 (MIT) is fetched by
ports/uacpi, checksum-verified, and compiled into the kernel unmodified,
the same way lwIP is. kernel/acpi/host.c is its host layer: memory,
mapping, port and PCI config I/O, time, mutexes and events that spin
before the scheduler exists, the SCI, and an "acpi" thread for the
work the SCI defers.

PCI interrupts go where the board says. pci_init() now routes every
device's INTx through the root bridge's _PRT before drivers bind. It
follows link devices' _CRS, uses a bridge's own _PRT where the namespace
has one, and swizzles through bridges that don't. Before this, INTx
waited on the 8259's line number, which is the wrong pin in APIC mode.
irq_register() takes a trigger mode rather than a level/low flag,
because q35's links and most SCIs are level, active-high. The MADT's
interrupt source overrides are now parsed, and are what places the SCI.

The power button syncs the filesystems and powers off. shutdown() and
reboot() go through the firmware's S5 and reset register first, and
only then fall back to the emulators' ports, which a real board does
not answer.

Tested under QEMU: q35 with MSI off routes xHCI to GSI 22,
level/active-high, and it takes real interrupts with nothing polled.
system_powerdown powers off q35 and i440fx (QEMU exits 0), and q35
with 4 CPUs boots from NVMe with USB hot-plug working alongside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-22 16:12:28 +02:00

119 lines
3.6 KiB
C

// The I/O APIC manages hardware interrupts for an SMP system.
// http://www.intel.com/design/chipsets/datashts/29056601.pdf
// See also picirq.c.
#include "types.h"
#include "defs.h"
#include "traps.h"
#include "memlayout.h"
#include "mmu.h"
#include "mp.h"
#include "param.h"
#include "proc.h"
#define REG_ID 0x00 // Register index: ID
#define REG_VER 0x01 // Register index: version
#define REG_TABLE 0x10 // Redirection table base
// The redirection table starts at REG_TABLE and uses
// two registers to configure each interrupt.
// The first (low) register in a pair contains configuration bits.
// The second (high) register contains a bitmask telling which
// CPUs can serve that interrupt.
#define INT_DISABLED 0x00010000 // Interrupt disabled
#define INT_LEVEL 0x00008000 // Level-triggered (vs edge-)
#define INT_ACTIVELOW 0x00002000 // Active low (vs high)
#define INT_LOGICAL 0x00000800 // Destination is CPU id (vs APIC ID)
volatile struct ioapic *ioapic;
// IO APIC MMIO structure: write reg, then read or write data.
struct ioapic {
uint reg;
uint pad[3];
uint data;
};
static uint
ioapicread(int reg)
{
ioapic->reg = reg;
return ioapic->data;
}
static void
ioapicwrite(int reg, uint data)
{
ioapic->reg = reg;
ioapic->data = data;
}
void
ioapicinit(void)
{
int i, id, maxintr;
// The firmware's address for it, not a constant: see ioapic_phys in mp.c.
// ioremap() rather than P2V because nothing obliges it to be in the low
// four gigabytes the direct map is sure to cover.
ioapic = ioremap(ioapic_phys, PGSIZE);
if(!ioapic)
panic("ioapicinit: cannot map the I/O APIC");
maxintr = (ioapicread(REG_VER) >> 16) & 0xFF;
id = ioapicread(REG_ID) >> 24;
if(id != ioapicid)
cprintf("ioapicinit: id isn't equal to ioapicid; not a MP\n");
// Mark all interrupts edge-triggered, active high, disabled,
// and not routed to any CPUs.
for(i = 0; i <= maxintr; i++){
ioapicwrite(REG_TABLE+2*i, INT_DISABLED | (T_IRQ0 + i));
ioapicwrite(REG_TABLE+2*i+1, 0);
}
}
// Route an interrupt to a CPU and unmask it.
//
// `level_low` picks PCI's signalling instead of ISA's. This distinction did
// not matter while every interrupt in the kernel came from an ISA-era device
// at a fixed pin, all of which are edge-triggered and active high -- which is
// why the original ioapicenable() hardcoded exactly that. A PCI device wired
// through an INTx pin is the opposite on both counts, and configuring it as
// edge/high does not fail cleanly: the device raises the line, we take one
// interrupt, and because the level never goes away there is no second edge
// and the device is never heard from again.
void
ioapicroute(int irq, int cpunum, int level_low)
{
uint lo = T_IRQ0 + irq;
// A mode from include/irq.h; 1, the PCI case, is what `level_low` meant
// when it was a flag.
if(level_low == 1 || level_low == 2)
lo |= INT_LEVEL;
if(level_low == 1 || level_low == 3)
lo |= INT_ACTIVELOW;
// Program the destination before unmasking, so the entry is never briefly
// live pointing at CPU 0 when it was meant for another.
//
// The destination is an APIC *ID*, not a CPU index. This wrote the index,
// which is the same number only when the boot processor's APIC ID is 0 --
// true under every emulator and on most boards, and not promised: an
// interrupt aimed at an ID nobody has is an interrupt nobody receives.
ioapicwrite(REG_TABLE+2*irq+1, (uint)cpus[cpunum].apicid << 24);
ioapicwrite(REG_TABLE+2*irq, lo);
}
void
ioapicmask(int irq)
{
ioapicwrite(REG_TABLE+2*irq, INT_DISABLED | (T_IRQ0 + irq));
}
void
ioapicenable(int irq, int cpunum)
{
ioapicroute(irq, cpunum, 0);
}