// GICv3 + IRQ handling for QEMU's `-M virt`. // // ABI mirrors src/board/rpi4b/irq.zig: // * show_invalid_entry_message(typ, esr, address) — exception print // * enable_interrupt_gic(intid, core) — distributor enable + route // * handle_irq() — dispatcher // Plus a Zig-side inline-able board_irq_init() that brings the CPU // interface and the local redistributor up; kernel.zig calls it after // irq_init_vectors. Pi's equivalent is an empty inline fn. // // MMIO map (per `qemu-system-aarch64 -M virt -d unimp`): // * Distributor (GICD) @ 0x08000000 // * Redistributor for core 0 @ 0x080A0000 // CPU interface uses ICC_*_EL1 system registers (per the GICv3 spec). // // Two interrupts are dispatched today: the ARM generic non-secure // physical timer (PPI 14, INTID 30) and the PL011 console RX // (SPI 1, INTID 33). PSCI-driven SMP and any further peripherals // extend the switch in handle_irq. const Dtb = #import("virt_dtb").Dtb const uart = #import("virt_uart") const LINEAR_MAP_BASE u64 = 0xFFFF000000000000 // All four hardware-locator constants below are mutable so // board_irq_init can refresh them from the DTB the bootloader // handed off (UEFI / QEMU `-kernel`). Fallbacks match QEMU virt's // well-known layout so QEMU boots even when no DTB was passed. var gicd_base_pa u64 = 0x08000000 var gicr_base_pa u64 = 0x080A0000 var ns_phys_timer_irq u32 = 30 // ARM generic timer (PPI 14 → INTID 30) var pl011_irq u32 = 33 // PL011 console RX (SPI 1 → INTID 33) inline fn gicdIsenabler(n usize) *mut volatile u32 { return #ptrFromInt(gicd_base_pa + LINEAR_MAP_BASE + 0x100 + n * 4) } inline fn gicdIrouter(n usize) *mut volatile u64 { return #ptrFromInt(gicd_base_pa + LINEAR_MAP_BASE + 0x6000 + n * 8) } inline fn gicrWaker() *mut volatile u32 { return #ptrFromInt(gicr_base_pa + LINEAR_MAP_BASE + 0x0014) } const GICR_WAKER_PROCESSOR_SLEEP u32 = 1 << 1 const GICR_WAKER_CHILDREN_ASLEEP u32 = 1 << 2 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 handle_generic_timer() void extern fn timer_tick() void extern fn get_core() u32 extern var current *mut anyopaque const console = #import("console") // -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") } /// CPU-side GICv3 bring-up for the calling core. Must run after /// irq_init_vectors() and before any interrupt fires. /// 0. Pull GIC distributor / redistributor / timer-IRQ / PL011-IRQ /// values from the DTB if the bootloader handed one off. /// 1. ICC_SRE_EL1 |= 1 — enable system-register CPU interface /// 2. ICC_PMR_EL1 = 0xFF — accept any priority /// 3. ICC_IGRPEN1_EL1 = 1 — enable Group-1 NS interrupts /// 4. Wake the local redistributor (GICR_WAKER): clear /// ProcessorSleep, then poll until ChildrenAsleep clears. pub fn board_irq_init() void { if Dtb.fromHandoff() |dtb| { if dtb.findRegN("arm,gic-v3", 0) |b| { gicd_base_pa = b } if dtb.findRegN("arm,gic-v3", 1) |b| { gicr_base_pa = b } if dtb.findInterrupt("arm,armv8-timer") |i| { ns_phys_timer_irq = i } } pl011_irq = uart.pl011Irq() _ = asm volatile ( \\mrs %[tmp], S3_0_C12_C12_5 \\orr %[tmp], %[tmp], #1 \\msr S3_0_C12_C12_5, %[tmp] \\isb : [tmp] "=&r" (-> u64), ) asm volatile ("msr S3_0_C4_C6_0, %[v]" : : [v] "r" (#as(u64, 0xFF)), ) asm volatile ( \\msr S3_0_C12_C12_7, %[v] \\isb : : [v] "r" (#as(u64, 1)), ) const waker = gicrWaker() waker.* = waker.* & ~GICR_WAKER_PROCESSOR_SLEEP while ((waker.* & GICR_WAKER_CHILDREN_ASLEEP) != 0) {} } /// Enable an interrupt at the GIC distributor and route SPIs /// (intid >= 32) to core 0. PPIs (intid 16..31) are private to each /// core's redistributor and don't need an IROUTER write. export fn enable_interrupt_gic(intid u32, core u32) void { _ = core const n usize = #intCast(intid / 32) const shift u5 = #intCast(intid % 32) gicdIsenabler(n).* = #as(u32, 1) << shift if (intid >= 32) { const router_n usize = #intCast(intid - 32) gicdIrouter(router_n).* = 0 // affinity = 0.0.0.0, IRM=0 (route to specific 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) var iar u64 = undefined asm volatile ("mrs %[iar], S3_0_C12_C12_0" : [iar] "=r" (iar), ) const intid u32 = #intCast(iar & 0xFFFFFF) // bits[23:0] // ns_phys_timer_irq and pl011_irq are runtime values populated // from the DTB by board_irq_init, so dispatch via if/else // instead of a comptime-only `switch`. if (intid == ns_phys_timer_irq) { handle_generic_timer() asm volatile ("msr S3_0_C12_C12_1, %[iar]" : : [iar] "r" (iar), ) if (get_core() == 0) { timer_tick() } } else if (intid == pl011_irq) { // Drain the PL011 RX FIFO in one IRQ slot. The line is // level-triggered: while RXFE is clear and RXIM is set, the // GIC keeps re-asserting — drain-to-empty quiesces it // without an explicit ICR write. console_push ring-buffers // the bytes and wakes the sys_readConsole waiter. while (uart.pl011_rx_pending()) { console.console_push(uart.mini_uart_recv()) } asm volatile ("msr S3_0_C12_C12_1, %[iar]" : : [iar] "r" (iar), ) } else { main_output(MU, "unknown pending irq\n") } }