ajhahn.de
← FlashOS
Flash 199 lines
// Interrupt handling — GIC (Generic Interrupt Controller) for Raspberry Pi 4

const LINEAR_MAP_BASE u64 = 0xFFFF000000000000
const GIC_BASE u64 = 0xFF840000 + LINEAR_MAP_BASE
const GICD_BASE u64 = GIC_BASE + 0x1000
const GICC_BASE u64 = GIC_BASE + 0x2000

const GICD_ISENABLER_BASE u64 = GICD_BASE + 0x100
const GICD_ITARGETSR_BASE u64 = GICD_BASE + 0x800
const GICC_CTLR u64 = GICC_BASE + 0x00
const GICC_PMR u64 = GICC_BASE + 0x04
const GICC_IAR u64 = GICC_BASE + 0x0C
const GICC_EOIR u64 = GICC_BASE + 0x10

const DistributorEnableRegs = extern struct { bitmap [32]u32 }
const DistributorTargetRegs = extern struct { set [255]u32 }

fn enableRegs() *mut volatile DistributorEnableRegs {
    return #as(*mut volatile DistributorEnableRegs, #ptrFromInt(GICD_ISENABLER_BASE))
}
fn targetRegs() *mut volatile DistributorTargetRegs {
    return #as(*mut volatile DistributorTargetRegs, #ptrFromInt(GICD_ITARGETSR_BASE))
}
fn iarReg() *mut volatile u32 {
    return #as(*mut volatile u32, #ptrFromInt(GICC_IAR))
}
fn eoirReg() *mut volatile u32 {
    return #as(*mut volatile u32, #ptrFromInt(GICC_EOIR))
}

// IRQ numbers
const NS_PHYS_TIMER_IRQ u32 = 30
const VC_TIMER_IRQ_1 u32 = 97
const VC_AUX_IRQ u32 = 125

const MU i32 = 0

extern fn main_output(interface i32, str [*:0]u8) void
extern fn main_output_u64(interface i32, n u64) void
extern fn main_output_char(interface i32, ch u8) void
extern fn main_output_process(interface i32, p *mut anyopaque) void
extern fn mini_uart_recv() u8
extern fn mini_uart_rx_pending() bool
extern fn handle_sys_timer_1() void
extern fn handle_generic_timer() void
extern fn timer_tick() void
extern fn get_core() u32
extern var current *mut anyopaque

const console = #import("console")
// Named module (the same instance board.zig exposes as board.usb): the
// timer-tick enumeration service below polls the DWC2 core.
const usb = #import("rpi4b_usb")

// -Dtrace profiler seam. The empty stub keeps handle_irq's signature and
// `frame` argument identical in a non-trace build (the call inlines to
// nothing and emits no code), so the default kernel image is byte-for-byte
// unchanged; only under -Dtrace does the sampler get pulled in.
const build_options = #import("build_options")
const KeRegs = #import("task_layout").KeRegs
const trace_sampler = if (build_options.trace)
    #import("sampler")
else
    struct {
        pub fn trace_sample(_ *mut KeRegs) void {}
    }

const entry_error_messages = [_][*:0]u8{
    "SYNC_INVALID_EL1t",
    "IRQ_INVALID_EL1t",
    "FIQ_INVALID_EL1t",
    "SERROR_INVALID_EL1t",
    "SYNC_INVALID_EL1h",
    "IRQ_INVALID_EL1h",
    "FIQ_INVALID_EL1h",
    "SERROR_INVALID_EL1h",
    "SYNC_INVALID_EL0_64",
    "IRQ_INVALID_EL0_64",
    "FIQ_INVALID_EL0_64",
    "SERROR_INVALID_EL0_64",
    "SYNC_INVALID_EL0_32",
    "IRQ_INVALID_EL0_32",
    "FIQ_INVALID_EL0_32",
    "SERROR_INVALID_EL0_32",
    "SYNC_ERROR",
    "SYSCALL_ERROR",
    "DATA_ABORT_ERROR"
}

export fn show_invalid_entry_message(typ u32, esr u64, address u64) void {
    main_output(MU, "ERROR CAUGHT: ")
    if (typ < entry_error_messages.len) {
        main_output(MU, entry_error_messages[typ])
    } else {
        main_output(MU, "UNKNOWN_ENTRY")
    }
    main_output(MU, ", ESR: ")
    main_output_u64(MU, esr)
    main_output(MU, ", Address: ")
    main_output_u64(MU, address)
    main_output(MU, "\n")
}

export fn enable_gic_distributor(intid u32) void {
    const n usize = #intCast(intid / 32)
    const shift u5 = #intCast(intid % 32)
    enableRegs().bitmap[n] |= (#as(u32, 1) << shift)
}

export fn assign_interrupt_core(intid u32, core u32) void {
    const n usize = #intCast(intid / 4)
    const byte_offset u32 = intid % 4
    const shift u5 = #intCast(byte_offset * 8 + core)
    targetRegs().set[n] |= (#as(u32, 1) << shift)
}

export fn enable_interrupt_gic(intid u32, core u32) void {
    enable_gic_distributor(intid)
    assign_interrupt_core(intid, core)
}

export fn handle_irq(frame *mut KeRegs) void {
    // Sample before dispatch so a tick that reschedules cannot skip it.
    // No-op (and no codegen) unless built with -Dtrace.
    trace_sampler.trace_sample(frame)

    const iar u32 = iarReg().*
    // GICv2 GICC_IAR INTID is bits[9:0]; mask is 0x3FF. A 0x2FF mask
    // silently clears bit 8 and drops IRQ IDs 256..511.
    const intid u32 = iar & 0x3FF
    switch intid {
        VC_TIMER_IRQ_1 => {
            handle_sys_timer_1()
            eoirReg().* = iar
        },
        VC_AUX_IRQ => {
            // Drain the entire RX FIFO in one IRQ slot. mini-UART FIFO
            // is 8 bytes on BCM2711; popping just one per IRQ would
            // lose bytes under sustained typing bursts since the level-
            // triggered AUX line refires only once per CPU-mask/unmask
            // round-trip. console_push ring-buffers + wakes the
            // sys_readConsole waiter.
            while (mini_uart_rx_pending()) {
                console.console_push(mini_uart_recv())
            }
            eoirReg().* = iar
        },
        NS_PHYS_TIMER_IRQ => {
            handle_generic_timer()
            eoirReg().* = iar
            if (get_core() == 0) {
                // USB service backstop. Enumeration never depends
                // on this 1 Hz tick — it could not meet the host's ~20 ms
                // post-reset SETUP window anyway; the usb.zig connection
                // manager keeps the gadget detached until the PID-0 idle
                // loop polls at µs rate. This backstop only matters when the
                // system goes busy AFTER attach: it keeps USBRST/ENUMDONE
                // state moving so the connection manager can self-heal once
                // the system is idle again. Gated on !enumerated(): once the
                // data path is up, IRQ-context polling would race
                // serviceTxRing against a syscall-context cdc_tx mid-FIFO-
                // write (preempt_disable does not mask IRQs). Runs BEFORE
                // timer_tick so a tick-triggered reschedule cannot skip it.
                if (!usb.enumerated()) {
                    usb.poll()
                }
                timer_tick()
            }
        },
        else => main_output(MU, "unknown pending irq\n"),
    }
}

/// CPU-side GICv2 (GIC-400) bring-up for the calling core — the rpi4b analog of
/// virt's GICv3 board_irq_init. FlashOS runs at non-secure EL1, so these MMIO
/// accesses hit the NS-banked CPU interface: GICC_CTLR bit 0 = EnableGrp1, and
/// GICC_PMR is the NS priority mask.
///   1. GICC_PMR  = 0xF0  — accept any priority. FlashOS uses a single
///                          interrupt priority, so this can never narrow
///                          delivery below the working set.
///   2. GICC_CTLR |= 1    — OR-in the Group-1 enable. Read-modify-write, NOT a
///                          plain assign: GICv2's GICC_CTLR packs firmware-owned
///                          bits (EOImode / CBPR / bypass, plus GIC-400 group /
///                          security bits) that a blind write would clobber, and
///                          an OR can only ever SET the enable — it cannot
///                          disable a CPU interface firmware already brought up,
///                          so it is regression-safe whatever the firmware left.
/// On the current Pi boot path firmware leaves the interface enabled, so this is
/// a self-healing no-op that removes the silent dependency on that firmware
/// state. NOTE: QEMU raspi4b pre-enables the CPU interface, so the watchdog
/// cannot tell a correct enable from a wrong one here — real-Pi boot acceptance
/// stays authoritative for this path.
pub fn board_irq_init() void {
    const gicc_pmr = #as(*mut volatile u32, #ptrFromInt(GICC_PMR))
    const gicc_ctlr = #as(*mut volatile u32, #ptrFromInt(GICC_CTLR))
    gicc_pmr.* = 0xF0
    gicc_ctlr.* |= 0x1
}