// 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 }