From 8d4b581519cc7b9332c9426693e8207359ae9bbf Mon Sep 17 00:00:00 2001 From: Markus Maiwald Date: Sun, 15 Feb 2026 19:58:51 +0100 Subject: [PATCH] feat(hal): ARM64 port, VirtIO MMIO, dual-arch HAL (M3.1-M3.3) --- apps/linker_user.ld | 4 + apps/linker_user_aarch64.ld | 44 ++ apps/subject_entry.S | 10 +- boot/header.zig | 112 +++- boot/linker_aarch64.ld | 54 ++ hal/abi.zig | 33 +- hal/channel.zig | 11 +- hal/cspace.zig | 13 + hal/entry_aarch64.zig | 1060 +++++++++++++++++++++++++++++++++++ hal/entry_riscv.zig | 9 +- hal/gic.zig | 169 ++++++ hal/mm.zig | 6 +- hal/ontology.zig | 8 +- hal/stubs.zig | 21 +- hal/stubs_user.zig | 50 +- hal/uart.zig | 94 ++++ hal/uart_input.zig | 4 + hal/virtio_block.zig | 114 +++- hal/virtio_mmio.zig | 268 +++++++++ hal/virtio_net.zig | 162 +++--- hal/virtio_pci.zig | 69 +++ 21 files changed, 2141 insertions(+), 174 deletions(-) create mode 100644 apps/linker_user_aarch64.ld create mode 100644 boot/linker_aarch64.ld create mode 100644 hal/entry_aarch64.zig create mode 100644 hal/gic.zig create mode 100644 hal/virtio_mmio.zig diff --git a/apps/linker_user.ld b/apps/linker_user.ld index a8f3ec1..5cd5b5c 100644 --- a/apps/linker_user.ld +++ b/apps/linker_user.ld @@ -33,6 +33,10 @@ SECTIONS *(.data.*) } > RAM + .nexus.manifest : { + KEEP(*(.nexus.manifest)) + } > RAM + .bss : { . = ALIGN(8); __bss_start = .; diff --git a/apps/linker_user_aarch64.ld b/apps/linker_user_aarch64.ld new file mode 100644 index 0000000..3250668 --- /dev/null +++ b/apps/linker_user_aarch64.ld @@ -0,0 +1,44 @@ +/* Memory Layout — ARM64 Cellular Memory (M3.3): + * User RAM: 0x48000000 - 0x4FFFFFFF (128MB) + * Stack starts at 0x4BFFFFF0 and grows down + * QEMU virt: -m 512M ensures valid physical backing + */ +MEMORY +{ + RAM (rwx) : ORIGIN = 0x48000000, LENGTH = 128M +} + +SECTIONS +{ + . = 0x48000000; + + .text : { + *(.text._start) + *(.text) + *(.text.*) + } > RAM + + .rodata : { + *(.rodata) + *(.rodata.*) + } > RAM + + .data : { + *(.data) + *(.data.*) + } > RAM + + .nexus.manifest : { + KEEP(*(.nexus.manifest)) + } > RAM + + .bss : { + . = ALIGN(8); + __bss_start = .; + *(.bss) + *(.bss.*) + *(COMMON) + . = ALIGN(8); + __bss_end = .; + } > RAM +} diff --git a/apps/subject_entry.S b/apps/subject_entry.S index fcc6ba4..75834a4 100644 --- a/apps/subject_entry.S +++ b/apps/subject_entry.S @@ -11,9 +11,15 @@ _start: 2: fence rw, rw - # Arguments (argc, argv) are already in a0, a1 from Kernel - # sp is already pointing to argc from Kernel + # Valid Args from Stack (Linux ABI) + ld a0, 0(sp) # argc + addi a1, sp, 8 # argv + # Calculate envp in a2: envp = argv + (argc + 1) * 8 + addi t0, a0, 1 # t0 = argc + 1 + slli t0, t0, 3 # t0 = (argc + 1) * 8 + add a2, a1, t0 # a2 = argv + offset + call main # Call exit(result) diff --git a/boot/header.zig b/boot/header.zig index 0765659..ab448f3 100644 --- a/boot/header.zig +++ b/boot/header.zig @@ -7,15 +7,16 @@ //! Rumpk Boot Header //! -//! Defines the Multiboot2 header for GRUB/QEMU and the bare-metal entry point. -//! Handles BSS clearing and stack initialization before jumping to the Nim kernel. +//! Architecture-dispatched entry point for bare-metal boot. +//! Handles BSS clearing and stack initialization before jumping to HAL init. //! //! SAFETY: Executed in the earliest boot stage with no environment initialized. const std = @import("std"); +const builtin = @import("builtin"); // ========================================================= -// Multiboot2 Header (for GRUB/QEMU) +// Multiboot2 Header (for GRUB/QEMU — x86 only) // ========================================================= const MULTIBOOT2_MAGIC: u32 = 0xe85250d6; @@ -43,39 +44,92 @@ export const multiboot2_header linksection(".multiboot2") = Multiboot2Header{ }; // ========================================================= -// Entry Point +// Arch-Specific HAL Entry Points // ========================================================= extern fn riscv_init() noreturn; +extern fn aarch64_init() void; // Returns void (calls rumpk_halt internally) + +// ========================================================= +// Entry Point (Architecture Dispatched) +// ========================================================= // 1MB Kernel Stack const STACK_SIZE = 0x100000; export var kernel_stack: [STACK_SIZE]u8 align(16) linksection(".bss.stack") = undefined; export fn _start() callconv(.naked) noreturn { - // Clear BSS, set up stack, then jump to RISC-V Init - asm volatile ( - \\ // Set up stack - \\ la sp, kernel_stack - \\ li t0, %[stack_size] - \\ add sp, sp, t0 - \\ - \\ // Clear BSS - \\ la t0, __bss_start - \\ la t1, __bss_end - \\1: - \\ bge t0, t1, 2f - \\ sd zero, (t0) - \\ addi t0, t0, 8 - \\ j 1b - \\2: - \\ // Jump to HAL Init - \\ call riscv_init - \\ - \\ // Should never return - \\ wfi - \\ j 2b - : - : [stack_size] "i" (STACK_SIZE), - ); + switch (builtin.cpu.arch) { + .riscv64 => { + asm volatile ( + \\ // Set up stack + \\ la sp, kernel_stack + \\ li t0, %[stack_size] + \\ add sp, sp, t0 + \\ + \\ // Clear BSS + \\ la t0, __bss_start + \\ la t1, __bss_end + \\1: + \\ bge t0, t1, 2f + \\ sd zero, (t0) + \\ addi t0, t0, 8 + \\ j 1b + \\2: + \\ // Jump to RISC-V HAL Init + \\ call riscv_init + \\ + \\ // Should never return + \\ wfi + \\ j 2b + : + : [stack_size] "i" (STACK_SIZE), + ); + }, + .aarch64 => { + asm volatile ( + // Mask all exceptions + \\ msr daifset, #0xf + // + // Enable FP/SIMD (CPACR_EL1.FPEN = 0b11) + \\ mov x0, #(3 << 20) + \\ msr cpacr_el1, x0 + \\ isb + // + // Disable alignment check (SCTLR_EL1.A = 0) + \\ mrs x0, sctlr_el1 + \\ bic x0, x0, #(1 << 1) + \\ msr sctlr_el1, x0 + \\ isb + // + // Set up stack + \\ adrp x0, kernel_stack + \\ add x0, x0, :lo12:kernel_stack + \\ mov x1, #0x100000 + \\ add sp, x0, x1 + // + // Clear BSS + \\ adrp x0, __bss_start + \\ add x0, x0, :lo12:__bss_start + \\ adrp x1, __bss_end + \\ add x1, x1, :lo12:__bss_end + \\ 1: cmp x0, x1 + \\ b.ge 2f + \\ str xzr, [x0], #8 + \\ b 1b + \\ 2: + // + // Jump to ARM64 HAL Init + \\ bl aarch64_init + // + // Should never return + \\ 3: wfe + \\ b 3b + ); + }, + else => { + // Unsupported architecture + unreachable; + }, + } } diff --git a/boot/linker_aarch64.ld b/boot/linker_aarch64.ld new file mode 100644 index 0000000..6462ea2 --- /dev/null +++ b/boot/linker_aarch64.ld @@ -0,0 +1,54 @@ +/* Rumpk Linker Script (AArch64) + * For QEMU virt machine (ARM64) + * Load address: 0x40080000 (QEMU -kernel default for virt) + */ + +ENTRY(_start) + +SECTIONS +{ + . = 0x40080000; + PROVIDE(__kernel_vbase = .); + PROVIDE(__kernel_pbase = .); + + .text : { + *(.text._start) + *(.text*) + } + + .rodata : { + *(.rodata*) + } + + .data : { + . = ALIGN(16); + *(.sdata*) + *(.sdata.*) + *(.data*) + } + + .initrd : { + _initrd_start = .; + KEEP(*(.initrd)) + _initrd_end = .; + } + + .bss : { + __bss_start = .; + *(.bss*) + *(COMMON) + __bss_end = .; + } + + .stack (NOLOAD) : { + . = ALIGN(16); + . += 0x100000; /* 1MB Stack */ + PROVIDE(__stack_top = .); + } + + /DISCARD/ : { + *(.comment) + *(.note*) + *(.eh_frame*) + } +} diff --git a/hal/abi.zig b/hal/abi.zig index 7717529..998b12c 100644 --- a/hal/abi.zig +++ b/hal/abi.zig @@ -55,9 +55,14 @@ fn halt_impl() callconv(.c) noreturn { } } -// ========================================================= -// Exports for Nim FFI -// ========================================================= +const builtin = @import("builtin"); + +// Sovereign timer — canonical time source for the entire kernel +extern fn rumpk_timer_now_ns() u64; + +export fn hal_get_time_ns() u64 { + return rumpk_timer_now_ns(); +} export fn rumpk_console_write(ptr: [*]const u8, len: usize) void { hal.console_write(ptr, len); @@ -113,17 +118,27 @@ pub const cspace_check_perm = cspace.cspace_check_perm; pub const surface = @import("surface.zig"); comptime { - // Force analysis + // Force analysis — architecture-independent modules _ = @import("stubs.zig"); - _ = @import("mm.zig"); _ = @import("channel.zig"); _ = @import("uart.zig"); - _ = @import("virtio_block.zig"); - _ = @import("virtio_net.zig"); - _ = @import("virtio_pci.zig"); _ = @import("ontology.zig"); - _ = @import("entry_riscv.zig"); _ = @import("cspace.zig"); _ = @import("surface.zig"); _ = @import("initrd.zig"); + + // Architecture-specific modules + if (builtin.cpu.arch == .riscv64) { + _ = @import("mm.zig"); + _ = @import("virtio_block.zig"); + _ = @import("virtio_net.zig"); + _ = @import("virtio_pci.zig"); + _ = @import("entry_riscv.zig"); + } else if (builtin.cpu.arch == .aarch64) { + _ = @import("entry_aarch64.zig"); + _ = @import("gic.zig"); + _ = @import("virtio_mmio.zig"); + _ = @import("virtio_block.zig"); + _ = @import("virtio_net.zig"); + } } diff --git a/hal/channel.zig b/hal/channel.zig index 948395d..69d5f25 100644 --- a/hal/channel.zig +++ b/hal/channel.zig @@ -13,6 +13,7 @@ //! SAFETY: All operations use atomic loads/stores with proper memory fences. const std = @import("std"); +const builtin = @import("builtin"); pub const IonPacket = extern struct { data: u64, @@ -41,8 +42,8 @@ pub fn Ring(comptime T: type) type { // INVARIANT 1: The Handle Barrier fn validate_ring_ptr(ptr: u64) void { - // 0x8000_0000 is kernel base, 0x8300_0000 is ION base. - if (ptr < 0x8000_0000) { + const min_valid: u64 = if (builtin.cpu.arch == .aarch64) 0x4000_0000 else 0x8000_0000; + if (ptr < min_valid) { @panic("HAL: Invariant Violation - Invalid Ring Pointer"); } } @@ -72,7 +73,11 @@ fn popGeneric(comptime T: type, ring: *Ring(T), out_pkt: *T) bool { } // Ensure we see data written by producer before reading it - asm volatile ("fence r, rw" ::: .{ .memory = true }); + switch (builtin.cpu.arch) { + .riscv64 => asm volatile ("fence r, rw" ::: .{ .memory = true }), + .aarch64 => asm volatile ("dmb ld" ::: .{ .memory = true }), + else => @compileError("unsupported arch"), + } out_pkt.* = ring.data[tail & ring.mask]; const next = (tail + 1) & ring.mask; diff --git a/hal/cspace.zig b/hal/cspace.zig index d897e92..4ffe4dd 100644 --- a/hal/cspace.zig +++ b/hal/cspace.zig @@ -230,6 +230,19 @@ pub export fn cspace_check_perm(fiber_id: u64, slot: usize, perm_bits: u8) bool return cap.has_perm(perm); } +/// Check if fiber has Channel capability for given channel_id with required permission (C ABI) +/// Scans all CSpace slots for a matching Channel capability by object_id. +pub export fn cspace_check_channel(fiber_id: u64, channel_id: u64, perm_bits: u8) bool { + const cs = cspace_get(fiber_id) orelse return false; + const perm: CapPerms = @bitCast(perm_bits); + for (&cs.slots) |*cap| { + if (cap.cap_type == .Channel and cap.object_id == channel_id and cap.has_perm(perm)) { + return true; + } + } + return false; +} + // Unit tests test "Capability creation and validation" { const cap = Capability{ diff --git a/hal/entry_aarch64.zig b/hal/entry_aarch64.zig new file mode 100644 index 0000000..bbfbc7f --- /dev/null +++ b/hal/entry_aarch64.zig @@ -0,0 +1,1060 @@ +// SPDX-License-Identifier: LCL-1.0 +// Copyright (c) 2026 Markus Maiwald +// Stewardship: Self Sovereign Society Foundation +// +// This file is part of the Nexus Commonwealth. +// See legal/LICENSE_COMMONWEALTH.md for license terms. + +//! Rumpk HAL: AArch64 Entry Point (Sovereign Trap Architecture) +//! +//! This is the hardware floor for ARM64. Sets up exception vectors, +//! GIC, Generic Timer, and PL011 UART before handing off to Nim. +//! +//! SAFETY: Runs in bare-metal EL1 with identity mapping (no MMU in M3.1). + +const std = @import("std"); +const uart = @import("uart.zig"); +const gic = @import("gic.zig"); +const uart_input = @import("uart_input.zig"); + +// ========================================================= +// L1 Kernel Logic (Nim FFI) +// ========================================================= + +extern fn k_handle_syscall(nr: usize, a0: usize, a1: usize, a2: usize) usize; +extern fn k_handle_exception(cause: usize, pc: usize, addr: usize) void; +extern fn k_check_deferred_yield() void; +extern fn kmain() void; +extern fn NimMain() void; +extern fn hal_surface_init() void; + +// ========================================================= +// Trap Frame (34 registers * 8 = 272 bytes, 16-byte aligned = 288) +// ========================================================= + +const TrapFrame = extern struct { + // x0-x30 (31 GPRs) + x: [31]usize, + // SP_EL0 (user stack pointer) + sp_el0: usize, + // Exception Link Register (return address) + elr_el1: usize, + // Saved Program Status Register + spsr_el1: usize, + // ESR_EL1 (Exception Syndrome) + esr_el1: usize, + // FAR_EL1 (Fault Address) + far_el1: usize, +}; + +// ========================================================= +// Exception Vector Table +// ========================================================= +// ARM64 requires 16 entries, each 128 bytes (32 instructions), aligned to 2048. +// Layout: +// [0x000] Current EL, SP0: Sync / IRQ / FIQ / SError +// [0x200] Current EL, SPx: Sync / IRQ / FIQ / SError <- kernel traps +// [0x400] Lower EL, AArch64: Sync / IRQ / FIQ / SError <- userland traps +// [0x600] Lower EL, AArch32: Sync / IRQ / FIQ / SError <- unused + +// Vector table is built at runtime by install_vectors_asm() — no comptime needed. + +// ========================================================= +// Vector Handlers (Assembly Trampolines) +// ========================================================= + +// Shared context save/restore macro as inline asm. +// Saves x0-x30, SP_EL0, ELR_EL1, SPSR_EL1, ESR_EL1, FAR_EL1 onto kernel stack. +// Total frame: 36 * 8 = 288 bytes (16-byte aligned). + +fn save_context() callconv(.naked) void { + asm volatile ( + // Allocate trap frame (288 bytes = 36 * 8) + \\ sub sp, sp, #288 + // Save x0-x30 + \\ stp x0, x1, [sp, #0] + \\ stp x2, x3, [sp, #16] + \\ stp x4, x5, [sp, #32] + \\ stp x6, x7, [sp, #48] + \\ stp x8, x9, [sp, #64] + \\ stp x10, x11, [sp, #80] + \\ stp x12, x13, [sp, #96] + \\ stp x14, x15, [sp, #112] + \\ stp x16, x17, [sp, #128] + \\ stp x18, x19, [sp, #144] + \\ stp x20, x21, [sp, #160] + \\ stp x22, x23, [sp, #176] + \\ stp x24, x25, [sp, #192] + \\ stp x26, x27, [sp, #208] + \\ stp x28, x29, [sp, #224] + \\ str x30, [sp, #240] + // Save SP_EL0 + \\ mrs x0, sp_el0 + \\ str x0, [sp, #248] + // Save ELR_EL1 + \\ mrs x0, elr_el1 + \\ str x0, [sp, #256] + // Save SPSR_EL1 + \\ mrs x0, spsr_el1 + \\ str x0, [sp, #264] + // Save ESR_EL1 + \\ mrs x0, esr_el1 + \\ str x0, [sp, #272] + // Save FAR_EL1 + \\ mrs x0, far_el1 + \\ str x0, [sp, #280] + // x0 = frame pointer (sp) + \\ mov x0, sp + \\ ret + ); +} + +fn restore_context() callconv(.naked) void { + asm volatile ( + // Restore ELR_EL1 + \\ ldr x0, [sp, #256] + \\ msr elr_el1, x0 + // Restore SPSR_EL1 + \\ ldr x0, [sp, #264] + \\ msr spsr_el1, x0 + // Restore SP_EL0 + \\ ldr x0, [sp, #248] + \\ msr sp_el0, x0 + // Restore x0-x30 + \\ ldp x0, x1, [sp, #0] + \\ ldp x2, x3, [sp, #16] + \\ ldp x4, x5, [sp, #32] + \\ ldp x6, x7, [sp, #48] + \\ ldp x8, x9, [sp, #64] + \\ ldp x10, x11, [sp, #80] + \\ ldp x12, x13, [sp, #96] + \\ ldp x14, x15, [sp, #112] + \\ ldp x16, x17, [sp, #128] + \\ ldp x18, x19, [sp, #144] + \\ ldp x20, x21, [sp, #160] + \\ ldp x22, x23, [sp, #176] + \\ ldp x24, x25, [sp, #192] + \\ ldp x26, x27, [sp, #208] + \\ ldp x28, x29, [sp, #224] + \\ ldr x30, [sp, #240] + // Deallocate frame + \\ add sp, sp, #288 + \\ eret + ); +} + +// Sync exception from current EL (kernel) +export fn vector_sync_handler() callconv(.naked) void { + asm volatile ( + \\ sub sp, sp, #288 + \\ stp x0, x1, [sp, #0] + \\ stp x2, x3, [sp, #16] + \\ stp x4, x5, [sp, #32] + \\ stp x6, x7, [sp, #48] + \\ stp x8, x9, [sp, #64] + \\ stp x10, x11, [sp, #80] + \\ stp x12, x13, [sp, #96] + \\ stp x14, x15, [sp, #112] + \\ stp x16, x17, [sp, #128] + \\ stp x18, x19, [sp, #144] + \\ stp x20, x21, [sp, #160] + \\ stp x22, x23, [sp, #176] + \\ stp x24, x25, [sp, #192] + \\ stp x26, x27, [sp, #208] + \\ stp x28, x29, [sp, #224] + \\ str x30, [sp, #240] + \\ mrs x0, sp_el0 + \\ str x0, [sp, #248] + \\ mrs x0, elr_el1 + \\ str x0, [sp, #256] + \\ mrs x0, spsr_el1 + \\ str x0, [sp, #264] + \\ mrs x0, esr_el1 + \\ str x0, [sp, #272] + \\ mrs x0, far_el1 + \\ str x0, [sp, #280] + \\ mov x0, sp + \\ bl rss_trap_handler + \\ ldr x0, [sp, #256] + \\ msr elr_el1, x0 + \\ ldr x0, [sp, #264] + \\ msr spsr_el1, x0 + \\ ldr x0, [sp, #248] + \\ msr sp_el0, x0 + \\ ldp x0, x1, [sp, #0] + \\ ldp x2, x3, [sp, #16] + \\ ldp x4, x5, [sp, #32] + \\ ldp x6, x7, [sp, #48] + \\ ldp x8, x9, [sp, #64] + \\ ldp x10, x11, [sp, #80] + \\ ldp x12, x13, [sp, #96] + \\ ldp x14, x15, [sp, #112] + \\ ldp x16, x17, [sp, #128] + \\ ldp x18, x19, [sp, #144] + \\ ldp x20, x21, [sp, #160] + \\ ldp x22, x23, [sp, #176] + \\ ldp x24, x25, [sp, #192] + \\ ldp x26, x27, [sp, #208] + \\ ldp x28, x29, [sp, #224] + \\ ldr x30, [sp, #240] + \\ add sp, sp, #288 + \\ eret + ); +} + +// IRQ from current EL (kernel) +export fn vector_irq_handler() callconv(.naked) void { + asm volatile ( + \\ sub sp, sp, #288 + \\ stp x0, x1, [sp, #0] + \\ stp x2, x3, [sp, #16] + \\ stp x4, x5, [sp, #32] + \\ stp x6, x7, [sp, #48] + \\ stp x8, x9, [sp, #64] + \\ stp x10, x11, [sp, #80] + \\ stp x12, x13, [sp, #96] + \\ stp x14, x15, [sp, #112] + \\ stp x16, x17, [sp, #128] + \\ stp x18, x19, [sp, #144] + \\ stp x20, x21, [sp, #160] + \\ stp x22, x23, [sp, #176] + \\ stp x24, x25, [sp, #192] + \\ stp x26, x27, [sp, #208] + \\ stp x28, x29, [sp, #224] + \\ str x30, [sp, #240] + \\ mrs x0, sp_el0 + \\ str x0, [sp, #248] + \\ mrs x0, elr_el1 + \\ str x0, [sp, #256] + \\ mrs x0, spsr_el1 + \\ str x0, [sp, #264] + \\ mrs x0, esr_el1 + \\ str x0, [sp, #272] + \\ mrs x0, far_el1 + \\ str x0, [sp, #280] + \\ mov x0, sp + \\ bl rss_trap_handler + \\ ldr x0, [sp, #256] + \\ msr elr_el1, x0 + \\ ldr x0, [sp, #264] + \\ msr spsr_el1, x0 + \\ ldr x0, [sp, #248] + \\ msr sp_el0, x0 + \\ ldp x0, x1, [sp, #0] + \\ ldp x2, x3, [sp, #16] + \\ ldp x4, x5, [sp, #32] + \\ ldp x6, x7, [sp, #48] + \\ ldp x8, x9, [sp, #64] + \\ ldp x10, x11, [sp, #80] + \\ ldp x12, x13, [sp, #96] + \\ ldp x14, x15, [sp, #112] + \\ ldp x16, x17, [sp, #128] + \\ ldp x18, x19, [sp, #144] + \\ ldp x20, x21, [sp, #160] + \\ ldp x22, x23, [sp, #176] + \\ ldp x24, x25, [sp, #192] + \\ ldp x26, x27, [sp, #208] + \\ ldp x28, x29, [sp, #224] + \\ ldr x30, [sp, #240] + \\ add sp, sp, #288 + \\ eret + ); +} + +// Sync exception from lower EL (userland SVC) +export fn vector_sync_lower() callconv(.naked) void { + // Same save/restore/eret pattern + asm volatile ( + \\ sub sp, sp, #288 + \\ stp x0, x1, [sp, #0] + \\ stp x2, x3, [sp, #16] + \\ stp x4, x5, [sp, #32] + \\ stp x6, x7, [sp, #48] + \\ stp x8, x9, [sp, #64] + \\ stp x10, x11, [sp, #80] + \\ stp x12, x13, [sp, #96] + \\ stp x14, x15, [sp, #112] + \\ stp x16, x17, [sp, #128] + \\ stp x18, x19, [sp, #144] + \\ stp x20, x21, [sp, #160] + \\ stp x22, x23, [sp, #176] + \\ stp x24, x25, [sp, #192] + \\ stp x26, x27, [sp, #208] + \\ stp x28, x29, [sp, #224] + \\ str x30, [sp, #240] + \\ mrs x0, sp_el0 + \\ str x0, [sp, #248] + \\ mrs x0, elr_el1 + \\ str x0, [sp, #256] + \\ mrs x0, spsr_el1 + \\ str x0, [sp, #264] + \\ mrs x0, esr_el1 + \\ str x0, [sp, #272] + \\ mrs x0, far_el1 + \\ str x0, [sp, #280] + \\ mov x0, sp + \\ bl rss_trap_handler + \\ ldr x0, [sp, #256] + \\ msr elr_el1, x0 + \\ ldr x0, [sp, #264] + \\ msr spsr_el1, x0 + \\ ldr x0, [sp, #248] + \\ msr sp_el0, x0 + \\ ldp x0, x1, [sp, #0] + \\ ldp x2, x3, [sp, #16] + \\ ldp x4, x5, [sp, #32] + \\ ldp x6, x7, [sp, #48] + \\ ldp x8, x9, [sp, #64] + \\ ldp x10, x11, [sp, #80] + \\ ldp x12, x13, [sp, #96] + \\ ldp x14, x15, [sp, #112] + \\ ldp x16, x17, [sp, #128] + \\ ldp x18, x19, [sp, #144] + \\ ldp x20, x21, [sp, #160] + \\ ldp x22, x23, [sp, #176] + \\ ldp x24, x25, [sp, #192] + \\ ldp x26, x27, [sp, #208] + \\ ldp x28, x29, [sp, #224] + \\ ldr x30, [sp, #240] + \\ add sp, sp, #288 + \\ eret + ); +} + +// IRQ from lower EL (userland interrupted) +export fn vector_irq_lower() callconv(.naked) void { + asm volatile ( + \\ sub sp, sp, #288 + \\ stp x0, x1, [sp, #0] + \\ stp x2, x3, [sp, #16] + \\ stp x4, x5, [sp, #32] + \\ stp x6, x7, [sp, #48] + \\ stp x8, x9, [sp, #64] + \\ stp x10, x11, [sp, #80] + \\ stp x12, x13, [sp, #96] + \\ stp x14, x15, [sp, #112] + \\ stp x16, x17, [sp, #128] + \\ stp x18, x19, [sp, #144] + \\ stp x20, x21, [sp, #160] + \\ stp x22, x23, [sp, #176] + \\ stp x24, x25, [sp, #192] + \\ stp x26, x27, [sp, #208] + \\ stp x28, x29, [sp, #224] + \\ str x30, [sp, #240] + \\ mrs x0, sp_el0 + \\ str x0, [sp, #248] + \\ mrs x0, elr_el1 + \\ str x0, [sp, #256] + \\ mrs x0, spsr_el1 + \\ str x0, [sp, #264] + \\ mrs x0, esr_el1 + \\ str x0, [sp, #272] + \\ mrs x0, far_el1 + \\ str x0, [sp, #280] + \\ mov x0, sp + \\ bl rss_trap_handler + \\ ldr x0, [sp, #256] + \\ msr elr_el1, x0 + \\ ldr x0, [sp, #264] + \\ msr spsr_el1, x0 + \\ ldr x0, [sp, #248] + \\ msr sp_el0, x0 + \\ ldp x0, x1, [sp, #0] + \\ ldp x2, x3, [sp, #16] + \\ ldp x4, x5, [sp, #32] + \\ ldp x6, x7, [sp, #48] + \\ ldp x8, x9, [sp, #64] + \\ ldp x10, x11, [sp, #80] + \\ ldp x12, x13, [sp, #96] + \\ ldp x14, x15, [sp, #112] + \\ ldp x16, x17, [sp, #128] + \\ ldp x18, x19, [sp, #144] + \\ ldp x20, x21, [sp, #160] + \\ ldp x22, x23, [sp, #176] + \\ ldp x24, x25, [sp, #192] + \\ ldp x26, x27, [sp, #208] + \\ ldp x28, x29, [sp, #224] + \\ ldr x30, [sp, #240] + \\ add sp, sp, #288 + \\ eret + ); +} + +// ========================================================= +// Trap Handler (Zig Logic) +// ========================================================= + +// ESR_EL1 Exception Class codes +const EC_SVC64: u6 = 0x15; // SVC in AArch64 +const EC_DATA_ABORT_LOWER: u6 = 0x24; +const EC_DATA_ABORT_SAME: u6 = 0x25; +const EC_INSN_ABORT_LOWER: u6 = 0x20; +const EC_INSN_ABORT_SAME: u6 = 0x21; + +var trap_depth: usize = 0; + +export fn rss_trap_handler(frame: *TrapFrame) void { + trap_depth += 1; + if (trap_depth > 3) { + uart.print("[Trap] Infinite Loop Detected. Halting.\n"); + while (true) { + asm volatile ("wfe"); + } + } + defer trap_depth -= 1; + + const esr = frame.esr_el1; + const ec: u6 = @truncate((esr >> 26) & 0x3F); + + // Determine if this came from IRQ vector or Sync vector + // by checking if ESR indicates an interrupt (EC=0 from IRQ vector) + // Actually, IRQ vectors call us too — but ESR won't have useful EC for IRQ. + // We use a simple heuristic: if called from IRQ vector, EC will be 0 typically. + // Better approach: check GIC for pending IRQs first. + + // Try to claim an IRQ — if one is pending, this is an interrupt + const irq = gic.gic_claim(); + if (!gic.is_spurious(irq)) { + // Interrupt path + if (irq == gic.TIMER_IRQ) { + // Timer interrupt: acknowledge and disable until rescheduled + timer_ack(); + k_check_deferred_yield(); + } else if (irq == gic.UART_IRQ) { + uart_input.poll_input(); + } else if (irq >= gic.VIRTIO_MMIO_IRQ_BASE and irq < gic.VIRTIO_MMIO_IRQ_BASE + 32) { + // VirtIO MMIO device interrupt — poll net driver + const virtio_net = @import("virtio_net.zig"); + virtio_net.virtio_net_poll(); + } + gic.gic_complete(irq); + return; + } + + // Synchronous exception path + if (ec == EC_SVC64) { + // Syscall: x8 = number, x0-x2 = args (ARM64 convention) + const nr = frame.x[8]; + const a0 = frame.x[0]; + const a1 = frame.x[1]; + const a2 = frame.x[2]; + + // Advance PC past SVC instruction + frame.elr_el1 += 4; + + const ret = k_handle_syscall(nr, a0, a1, a2); + frame.x[0] = ret; + } else if (ec == EC_DATA_ABORT_LOWER or ec == EC_DATA_ABORT_SAME or + ec == EC_INSN_ABORT_LOWER or ec == EC_INSN_ABORT_SAME) + { + uart.print("\n[Trap] Abort! EC:"); + uart.print_hex(@as(usize, ec)); + uart.print(" PC:"); + uart.print_hex(frame.elr_el1); + uart.print(" FAR:"); + uart.print_hex(frame.far_el1); + uart.print("\n"); + k_handle_exception(@as(usize, ec), frame.elr_el1, frame.far_el1); + while (true) { + asm volatile ("wfe"); + } + } else { + uart.print("\n[Trap] Unhandled EC:"); + uart.print_hex(@as(usize, ec)); + uart.print(" ESR:"); + uart.print_hex(esr); + uart.print(" PC:"); + uart.print_hex(frame.elr_el1); + uart.print("\n"); + } +} + +// ========================================================= +// ARM Generic Timer +// ========================================================= + +var cntfrq: u64 = 0; // Timer frequency (read at init) +var ns_per_tick_x16: u64 = 0; // (1e9 / freq) * 16 for fixed-point + +fn timer_init() void { + // Read timer frequency + cntfrq = asm volatile ("mrs %[ret], cntfrq_el0" + : [ret] "=r" (-> u64), + ); + + if (cntfrq == 0) { + // Fallback: QEMU virt typically uses 62.5 MHz + cntfrq = 62_500_000; + } + + // Precompute ns_per_tick * 16 for fixed-point math + // ns_per_tick = 1_000_000_000 / cntfrq + // We use * 16 to avoid floating point: (1e9 * 16) / cntfrq + ns_per_tick_x16 = (1_000_000_000 * 16) / cntfrq; + + // Disable timer initially + asm volatile ("msr cntp_ctl_el0, %[val]" + : + : [val] "r" (@as(u64, 0)), + ); +} + +fn timer_ack() void { + // Disable timer (mask) to prevent re-firing until rescheduled + asm volatile ("msr cntp_ctl_el0, %[val]" + : + : [val] "r" (@as(u64, 0x2)), // IMASK=1, ENABLE=0 + ); +} + +export fn rumpk_timer_now_ns() u64 { + const cnt: u64 = asm volatile ("mrs %[ret], cntpct_el0" + : [ret] "=r" (-> u64), + ); + // Convert to nanoseconds using precomputed fixed-point + // ns = cnt * ns_per_tick = cnt * (ns_per_tick_x16 / 16) + return (cnt * ns_per_tick_x16) >> 4; +} + +export fn rumpk_timer_set_ns(interval_ns: u64) void { + if (interval_ns == std.math.maxInt(u64)) { + // Disable timer + asm volatile ("msr cntp_ctl_el0, %[val]" + : + : [val] "r" (@as(u64, 0x2)), // IMASK=1 + ); + return; + } + + // Convert ns to ticks: ticks = ns * cntfrq / 1e9 + const ticks = (interval_ns * cntfrq) / 1_000_000_000; + + // Set countdown value and enable + asm volatile ("msr cntp_tval_el0, %[val]" + : + : [val] "r" (ticks), + ); + asm volatile ("msr cntp_ctl_el0, %[val]" + : + : [val] "r" (@as(u64, 0x1)), // ENABLE=1, IMASK=0 + ); +} + +// ========================================================= +// Identity Map (MMU Setup) +// ========================================================= +// ARM64 without MMU treats all memory as Device-nGnRnE, which requires +// strict alignment. We set up a minimal identity map with: +// MAIR index 0: Device-nGnRnE (0x00) — for MMIO +// MAIR index 1: Normal Write-Back Cacheable (0xFF) — for RAM +// Using 1GB block descriptors at Level 1 (only need L0 + L1 tables). + +// Page table storage (must be 4096-byte aligned) +// 39-bit VA (T0SZ=25) starts walk at L1 — no L0 needed +var l1_table: [512]u64 align(4096) = [_]u64{0} ** 512; + +fn setup_identity_map() void { + // MAIR_EL1: index 0 = Device-nGnRnE, index 1 = Normal WB Cacheable + const MAIR_VAL: u64 = 0xFF_00; // attr1=0xFF (Normal WB), attr0=0x00 (Device) + asm volatile ("msr mair_el1, %[val]" + : + : [val] "r" (MAIR_VAL), + ); + + // TCR_EL1: 4KB granule, 36-bit PA, T0SZ=25 (39-bit VA = 512GB) + // IPS=0b010 (40-bit PA), TG0=0b00 (4KB), SH0=0b11 (Inner Shareable), + // ORGN0=0b01 (WB Cacheable), IRGN0=0b01 (WB Cacheable), T0SZ=25 + const TCR_VAL: u64 = (0b010 << 32) | // IPS: 40-bit PA + (0b00 << 14) | // TG0: 4KB granule + (0b11 << 12) | // SH0: Inner Shareable + (0b01 << 10) | // ORGN0: Write-Back Cacheable + (0b01 << 8) | // IRGN0: Write-Back Cacheable + 25; // T0SZ: 39-bit VA space + asm volatile ("msr tcr_el1, %[val]" + : + : [val] "r" (TCR_VAL), + ); + + // With T0SZ=25, VA is 39 bits → translation starts at L1 (no L0 needed). + // L1 entry [38:30] = 9 bits → 512 entries, each 1GB block. + // TTBR0_EL1 points directly at the L1 table. + + // Block descriptor: addr[47:30] | AF | SH | AP | AttrIdx | Block(0b01) + const BLOCK_DEVICE: u64 = (1 << 10) | // AF (Access Flag) + (0b00 << 8) | // SH: Non-shareable (Device) + (0b00 << 6) | // AP: EL1 RW + (0b00 << 2) | // AttrIdx: 0 (Device-nGnRnE) + 0x1; // Block descriptor + + const BLOCK_NORMAL: u64 = (1 << 10) | // AF (Access Flag) + (0b11 << 8) | // SH: Inner Shareable + (0b00 << 6) | // AP: EL1 RW + (0b01 << 2) | // AttrIdx: 1 (Normal Cacheable) + 0x1; // Block descriptor + + // GB 0 (0x00000000-0x3FFFFFFF): Device (UART, GIC, etc.) + l1_table[0] = (0x00000000) | BLOCK_DEVICE; + // GB 1 (0x40000000-0x7FFFFFFF): Normal RAM (QEMU virt RAM) + l1_table[1] = (0x40000000) | BLOCK_NORMAL; + // GB 2 (0x80000000-0xBFFFFFFF): Device (high MMIO) + l1_table[2] = (0x80000000) | BLOCK_DEVICE; + // GB 3 (0xC0000000-0xFFFFFFFF): Device + l1_table[3] = (0xC0000000) | BLOCK_DEVICE; + + // Set TTBR0_EL1 to point at L1 table directly (39-bit VA starts at L1) + const l1_addr = @intFromPtr(&l1_table); + asm volatile ("msr ttbr0_el1, %[val]" + : + : [val] "r" (l1_addr), + ); + + // Invalidate TLB + asm volatile ("tlbi vmalle1"); + asm volatile ("dsb sy"); + asm volatile ("isb"); + + // Enable MMU + caches in SCTLR_EL1 + var sctlr: u64 = 0; + asm volatile ("mrs %[out], sctlr_el1" + : [out] "=r" (sctlr), + ); + sctlr |= (1 << 0); // M: Enable MMU + sctlr |= (1 << 2); // C: Enable data cache + sctlr |= (1 << 12); // I: Enable instruction cache + sctlr &= ~@as(u64, 1 << 1); // A: Disable alignment check + asm volatile ("msr sctlr_el1, %[val]" + : + : [val] "r" (sctlr), + ); + asm volatile ("isb"); +} + +// ========================================================= +// Entry Point +// ========================================================= + +// SAFETY(Stack): Memory is immediately used by _start before any read. +export var stack_bytes: [64 * 1024]u8 align(16) = undefined; + +export fn aarch64_init() void { + // 1. Initialize UART (PL011) + uart.init(); + uart.print("[Rumpk L0] aarch64_init reached\n"); + + // Set up identity-mapped page tables so RAM has Normal memory type. + // Without MMU, ARM64 uses Device memory which requires strict alignment. + setup_identity_map(); + uart.print("[Rumpk L0] Identity map + MMU enabled\n"); + + // 2. Initialize GIC + gic.gic_init(); + gic.gic_enable_timer_irq(); + uart.print("[Rumpk L0] GICv2 initialized\n"); + + // 3. Initialize Generic Timer + timer_init(); + uart.print("[Rumpk L0] Generic Timer initialized (freq="); + uart.print_hex(cntfrq); + uart.print(")\n"); + + // 4. Install exception vectors + // We write the vector table with proper branch instructions at runtime + install_vectors_asm(); + uart.print("[Rumpk L0] Exception vectors installed\n"); + + // 5. Enable IRQs (clear DAIF.I bit) + asm volatile ("msr daifclr, #0x2"); // Clear IRQ mask + + uart.print("[Rumpk ARM64] Handing off to Nim L1...\n"); + + // 6. Initialize Nim runtime and enter kernel + NimMain(); + kmain(); + rumpk_halt(); +} + +/// Install exception vectors using runtime assembly +/// This writes proper branch instructions into the vector table +fn install_vectors_asm() void { + // Set VBAR_EL1 to point at our vector handler functions + // We use a simpler approach: write a small vector table in a static buffer + // with branch instructions to our Zig handler functions. + + // The vector table entries need to branch to our handlers. + // ARM64 exception vectors: each entry is 128 bytes (0x80). + // We write `b ` at the start of each entry. + + // For the entries we care about: + // 0x200: Current EL SPx Sync -> vector_sync_handler + // 0x280: Current EL SPx IRQ -> vector_irq_handler + // 0x400: Lower EL Sync -> vector_sync_lower + // 0x480: Lower EL IRQ -> vector_irq_lower + + // We need the vector table to be 2048-byte aligned. + // Use our static vector_table_runtime buffer. + const table_addr = @intFromPtr(&vector_table_runtime); + + // Fill with WFE (halt) as default + const wfe_insn: u32 = 0xD503205F; // WFE + var i: usize = 0; + while (i < 2048) : (i += 4) { + const ptr: *volatile u32 = @ptrFromInt(table_addr + i); + ptr.* = wfe_insn; + } + + // Write branch instructions to our handlers + write_branch_to(table_addr + 0x200, @intFromPtr(&vector_sync_handler)); + write_branch_to(table_addr + 0x280, @intFromPtr(&vector_irq_handler)); + write_branch_to(table_addr + 0x400, @intFromPtr(&vector_sync_lower)); + write_branch_to(table_addr + 0x480, @intFromPtr(&vector_irq_lower)); + + // Set VBAR_EL1 + asm volatile ("msr vbar_el1, %[vbar]" + : + : [vbar] "r" (table_addr), + ); + asm volatile ("isb"); +} + +/// Runtime-writable vector table (2048 bytes, 2048-byte aligned) +var vector_table_runtime: [2048]u8 align(2048) = [_]u8{0} ** 2048; + +/// Write a branch instruction at `from` that jumps to `target` +fn write_branch_to(from: usize, target: usize) void { + // ARM64 B instruction: 0x14000000 | (imm26) + // imm26 is a signed offset in 4-byte units + const offset_bytes: i64 = @as(i64, @intCast(target)) - @as(i64, @intCast(from)); + const offset_words: i32 = @intCast(@divExact(offset_bytes, 4)); + const imm26: u32 = @as(u32, @bitCast(offset_words)) & 0x03FFFFFF; + const insn: u32 = 0x14000000 | imm26; + const ptr: *volatile u32 = @ptrFromInt(from); + ptr.* = insn; +} + +// ========================================================= +// HAL Exports (Contract with L1 Nim Kernel) +// ========================================================= + +export fn hal_console_write(ptr: [*]const u8, len: usize) void { + uart.write_bytes(ptr[0..len]); +} + +export fn console_read() c_int { + if (uart_input.read_byte()) |b| { + return @as(c_int, b); + } + return -1; +} + +export fn console_poll() void { + uart_input.poll_input(); +} + +export fn debug_uart_lsr() u8 { + return uart.get_lsr(); +} + +export fn uart_print_hex(value: u64) void { + uart.print_hex(value); +} + +export fn uart_print_hex8(value: u8) void { + uart.print_hex8(value); +} + +export fn hal_io_init() void { + uart.init(); + hal_surface_init(); + + // Initialize VirtIO block storage (MMIO transport) + const virtio_block = @import("virtio_block.zig"); + virtio_block.init(); +} + +export fn hal_panic(msg: [*:0]const u8) callconv(.c) noreturn { + uart.print("[HAL PANIC] "); + uart.print(std.mem.span(msg)); + uart.print("\n"); + rumpk_halt(); +} + +export fn rumpk_halt() noreturn { + uart.print("[Rumpk ARM64] Halting.\n"); + while (true) { + asm volatile ("wfe"); + } +} + +export fn hal_kexec(entry: u64, dtb: u64) noreturn { + _ = entry; + _ = dtb; + uart.print("[HAL] kexec not implemented on ARM64\n"); + rumpk_halt(); +} + +// ========================================================= +// Page Table Infrastructure (M3.3 — 4KB Granule, 39-bit VA) +// ========================================================= +// The identity map above uses 1GB block descriptors for early boot. +// For user isolation we need 4KB page granularity (L1→L2→L3 walk). + +const PAGE_SIZE: u64 = 4096; +const PAGE_SHIFT: u6 = 12; +const ENTRIES_PER_TABLE: usize = 512; + +// ARM64 descriptor bits +const DESC_VALID: u64 = 1 << 0; +const DESC_TABLE: u64 = 0b11; // L1/L2 table pointer +const DESC_PAGE: u64 = 0b11; // L3 page descriptor +const DESC_AF: u64 = 1 << 10; // Access Flag +const DESC_SH_ISH: u64 = 0b11 << 8; // Inner Shareable +const DESC_AP_RW_EL1: u64 = 0b00 << 6; // EL1 RW, EL0 no access +const DESC_AP_RW_ALL: u64 = 0b01 << 6; // EL1+EL0 RW +const DESC_UXN: u64 = @as(u64, 1) << 54; // Unprivileged Execute Never +const DESC_PXN: u64 = @as(u64, 1) << 53; // Privileged Execute Never +const ATTR_DEVICE: u64 = 0b00 << 2; // AttrIdx=0 (Device-nGnRnE) +const ATTR_NORMAL: u64 = 0b01 << 2; // AttrIdx=1 (Normal WB Cacheable) + +const DRAM_BASE: u64 = 0x40000000; + +// Bump allocator for page tables (8MB pool) +var pt_alloc_base: u64 = 0; +var pt_alloc_offset: u64 = 0; +const PT_POOL_SIZE: u64 = 8 * 1024 * 1024; + +fn pt_init_allocator(base: u64) void { + pt_alloc_base = base; + pt_alloc_offset = 0; +} + +/// Allocate one zeroed 4KB-aligned page table +fn pt_alloc() ?[*]u64 { + if (pt_alloc_offset + PAGE_SIZE > PT_POOL_SIZE) { + uart.print("[MM] Page table pool exhausted!\n"); + return null; + } + const addr = pt_alloc_base + pt_alloc_offset; + pt_alloc_offset += PAGE_SIZE; + + // Zero all 512 entries + const table: [*]volatile u64 = @ptrFromInt(addr); + for (0..ENTRIES_PER_TABLE) |i| { + table[i] = 0; + } + return @ptrFromInt(addr); +} + +/// Map a single 4KB page: walk L1→L2→L3, allocating intermediate tables +fn map_page(root: [*]u64, va: u64, pa: u64, attrs: u64) void { + // 39-bit VA with 4KB granule: + // L1 index = VA[38:30] (9 bits) + // L2 index = VA[29:21] (9 bits) + // L3 index = VA[20:12] (9 bits) + const l1_idx = (va >> 30) & 0x1FF; + const l2_idx = (va >> 21) & 0x1FF; + const l3_idx = (va >> 12) & 0x1FF; + + // L1 → L2 table + const l1_entry = root[l1_idx]; + const l2_table: [*]u64 = if (l1_entry & DESC_VALID != 0) + @ptrFromInt(l1_entry & 0x0000FFFFFFFFF000) + else blk: { + const new_l2 = pt_alloc() orelse return; + root[l1_idx] = @intFromPtr(new_l2) | DESC_TABLE; + break :blk new_l2; + }; + + // L2 → L3 table + const l2_entry = l2_table[l2_idx]; + const l3_table: [*]u64 = if (l2_entry & DESC_VALID != 0) + @ptrFromInt(l2_entry & 0x0000FFFFFFFFF000) + else blk: { + const new_l3 = pt_alloc() orelse return; + l2_table[l2_idx] = @intFromPtr(new_l3) | DESC_TABLE; + break :blk new_l3; + }; + + // L3 page descriptor: pa[47:12] | attrs | DESC_PAGE (0b11) + l3_table[l3_idx] = (pa & 0x0000FFFFFFFFF000) | attrs | DESC_PAGE; +} + +/// Map a range of pages (va and pa must be page-aligned) +fn map_range(root: [*]u64, va_start: u64, pa_start: u64, size: u64, attrs: u64) void { + var offset: u64 = 0; + while (offset < size) : (offset += PAGE_SIZE) { + map_page(root, va_start + offset, pa_start + offset, attrs); + } +} + +// ========================================================= +// HAL Userland Entry (EL1 → EL0 via eret) +// ========================================================= + +export fn hal_enter_userland(entry: u64, systable: u64, sp: u64) callconv(.c) void { + // SPSR_EL1 = 0 → return to EL0t (M[3:0]=0b0000), DAIF clear (IRQs enabled) + const spsr: u64 = 0; + + asm volatile ( + \\ msr spsr_el1, %[spsr] + \\ msr elr_el1, %[entry] + \\ msr sp_el0, %[sp] + \\ mov x0, %[systable] + \\ eret + : + : [spsr] "r" (spsr), + [entry] "r" (entry), + [sp] "r" (sp), + [systable] "r" (systable), + ); +} + +// ========================================================= +// Memory Management (M3.3 — Full Page Tables) +// ========================================================= + +extern fn kprint(s: [*:0]const u8) void; +extern fn kprint_hex(n: u64) void; + +var kernel_ttbr0: u64 = 0; + +export fn mm_init() callconv(.c) void { + // Page table pool at DRAM_BASE + 240MB (same offset as RISC-V) + pt_init_allocator(DRAM_BASE + 240 * 1024 * 1024); +} + +export fn mm_enable_kernel_paging() callconv(.c) void { + // Identity map is already set up by setup_identity_map() using 1GB blocks. + // Store current TTBR0 for later restore after worker map switches. + asm volatile ("mrs %[out], ttbr0_el1" + : [out] "=r" (kernel_ttbr0), + ); +} + +export fn mm_get_kernel_satp() callconv(.c) u64 { + return kernel_ttbr0; +} + +export fn mm_create_worker_map( + stack_base: u64, + stack_size: u64, + packet_addr: u64, + phys_base: u64, + region_size: u64, +) callconv(.c) u64 { + const root = pt_alloc() orelse return 0; + + kprint("[MM] Cellular Map: phys_base="); + kprint_hex(phys_base); + kprint(" size="); + kprint_hex(region_size); + kprint("\n"); + + // Kernel attributes: EL1 RW, Normal cacheable, no EL0 access + const kern_attrs = DESC_AF | DESC_SH_ISH | DESC_AP_RW_EL1 | ATTR_NORMAL; + + // User attributes: EL1+EL0 RW, Normal cacheable + const user_attrs = DESC_AF | DESC_SH_ISH | DESC_AP_RW_ALL | ATTR_NORMAL; + + // Device attributes: EL1 only, Device memory + const dev_attrs = DESC_AF | DESC_AP_RW_EL1 | ATTR_DEVICE; + + // Shared attributes: EL1+EL0 RW, Normal cacheable (for SysTable/ION rings) + const shared_attrs = DESC_AF | DESC_SH_ISH | DESC_AP_RW_ALL | ATTR_NORMAL | DESC_UXN; + + // 1. Kernel memory (0x40000000–0x48000000 = 128MB): EL1 only + // Allows kernel trap handlers to execute while worker map is active + map_range(root, DRAM_BASE, DRAM_BASE, 128 * 1024 * 1024, kern_attrs); + + // 2. User cell (identity mapped): EL0 accessible + // Init: VA 0x48000000 → PA 0x48000000 (64MB) + // Child: VA 0x48000000 → PA phys_base (varies) + const user_va_base = DRAM_BASE + 128 * 1024 * 1024; // 0x48000000 + map_range(root, user_va_base, phys_base, region_size, user_attrs); + + // 3. MMIO devices: EL1 only (kernel handles I/O) + map_range(root, 0x09000000, 0x09000000, PAGE_SIZE, dev_attrs); // PL011 UART + map_range(root, 0x08000000, 0x08000000, 0x20000, dev_attrs); // GICv2 + map_range(root, 0x0a000000, 0x0a000000, 0x200 * 32, dev_attrs); // VirtIO MMIO + + // 4. SysTable + ION rings: EL0 RW (256KB = 64 pages) + map_range(root, packet_addr, packet_addr, 64 * PAGE_SIZE, shared_attrs); + + // 5. Optional kernel stack mapping (if stack_base != 0) + if (stack_base != 0) { + map_range(root, stack_base, stack_base, stack_size, user_attrs); + } + + kprint("[MM] Worker map created successfully\n"); + + // Return TTBR0 value (physical address of root table) + // ARM64 TTBR has no mode bits like RISC-V SATP — just the address + return @intFromPtr(root); +} + +export fn mm_activate_satp(satp_val: u64) callconv(.c) void { + asm volatile ("msr ttbr0_el1, %[val]" + : + : [val] "r" (satp_val), + ); + asm volatile ("isb"); + asm volatile ("tlbi vmalle1"); + asm volatile ("dsb sy"); + asm volatile ("isb"); +} + +export fn mm_debug_check_va(va: u64) callconv(.c) void { + kprint("[MM] Inspecting VA: "); + kprint_hex(va); + kprint("\n"); + + // Read current TTBR0 + var ttbr0: u64 = 0; + asm volatile ("mrs %[out], ttbr0_el1" + : [out] "=r" (ttbr0), + ); + const root: [*]const u64 = @ptrFromInt(ttbr0 & 0x0000FFFFFFFFF000); + + const l1_idx = (va >> 30) & 0x1FF; + const l1_entry = root[l1_idx]; + kprint(" L1["); + kprint_hex(l1_idx); + kprint("]: "); + kprint_hex(l1_entry); + if (l1_entry & DESC_VALID == 0) { + kprint(" (Invalid)\n"); + return; + } + if (l1_entry & 0b10 == 0) { + kprint(" (Block)\n"); + return; + } + kprint(" (Table)\n"); + + const l2: [*]const u64 = @ptrFromInt(l1_entry & 0x0000FFFFFFFFF000); + const l2_idx = (va >> 21) & 0x1FF; + const l2_entry = l2[l2_idx]; + kprint(" L2["); + kprint_hex(l2_idx); + kprint("]: "); + kprint_hex(l2_entry); + if (l2_entry & DESC_VALID == 0) { + kprint(" (Invalid)\n"); + return; + } + if (l2_entry & 0b10 == 0) { + kprint(" (Block)\n"); + return; + } + kprint(" (Table)\n"); + + const l3: [*]const u64 = @ptrFromInt(l2_entry & 0x0000FFFFFFFFF000); + const l3_idx = (va >> 12) & 0x1FF; + const l3_entry = l3[l3_idx]; + kprint(" L3["); + kprint_hex(l3_idx); + kprint("]: "); + kprint_hex(l3_entry); + kprint("\n"); +} + +// VirtIO drivers now provided by virtio_net.zig and virtio_block.zig via abi.zig imports diff --git a/hal/entry_riscv.zig b/hal/entry_riscv.zig index 78bc65f..ab8649c 100644 --- a/hal/entry_riscv.zig +++ b/hal/entry_riscv.zig @@ -308,7 +308,7 @@ export fn rss_trap_handler(frame: *TrapFrame) void { const irq = PLIC_CLAIM.*; if (irq == 10) { // UART0 is IRQ 10 on Virt machine - // uart.print("[IRQ] 10\n"); + uart.print("[IRQ] 10\n"); uart_input.poll_input(); } else if (irq >= 32 and irq <= 35) { virtio_net.virtio_net_poll(); @@ -447,6 +447,13 @@ export fn hal_io_init() void { virtio_block.init(); } +export fn hal_panic(msg: [*:0]const u8) callconv(.c) noreturn { + uart.print("[HAL PANIC] "); + uart.print(std.mem.span(msg)); + uart.print("\n"); + rumpk_halt(); +} + export fn rumpk_halt() noreturn { uart.print("[Rumpk RISC-V] Halting.\n"); while (true) { diff --git a/hal/gic.zig b/hal/gic.zig new file mode 100644 index 0000000..59e95da --- /dev/null +++ b/hal/gic.zig @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: LCL-1.0 +// Copyright (c) 2026 Markus Maiwald +// Stewardship: Self Sovereign Society Foundation +// +// This file is part of the Nexus Commonwealth. +// See legal/LICENSE_COMMONWEALTH.md for license terms. + +//! Rumpk Layer 0: GICv2 Driver (ARM64) +//! +//! Minimal Generic Interrupt Controller v2 for QEMU virt machine. +//! Handles interrupt enable, claim, and complete for timer and device IRQs. +//! +//! SAFETY: All register accesses use volatile pointers to MMIO regions. + +// ========================================================= +// GICv2 MMIO Base Addresses (QEMU virt machine) +// ========================================================= + +const GICD_BASE: usize = 0x08000000; // Distributor +const GICC_BASE: usize = 0x08010000; // CPU Interface + +// ========================================================= +// Distributor Registers (GICD) +// ========================================================= + +const GICD_CTLR: usize = 0x000; // Control +const GICD_TYPER: usize = 0x004; // Type (read-only) +const GICD_ISENABLER: usize = 0x100; // Set-Enable (banked per 32 IRQs) +const GICD_ICENABLER: usize = 0x180; // Clear-Enable +const GICD_ISPENDR: usize = 0x200; // Set-Pending +const GICD_ICPENDR: usize = 0x280; // Clear-Pending +const GICD_IPRIORITYR: usize = 0x400; // Priority (byte-accessible) +const GICD_ITARGETSR: usize = 0x800; // Target (byte-accessible) +const GICD_ICFGR: usize = 0xC00; // Configuration + +// ========================================================= +// CPU Interface Registers (GICC) +// ========================================================= + +const GICC_CTLR: usize = 0x000; // Control +const GICC_PMR: usize = 0x004; // Priority Mask +const GICC_IAR: usize = 0x00C; // Interrupt Acknowledge +const GICC_EOIR: usize = 0x010; // End of Interrupt + +// ========================================================= +// IRQ Numbers (QEMU virt) +// ========================================================= + +/// Non-Secure Physical Timer PPI +pub const TIMER_IRQ: u32 = 30; + +/// UART PL011 (SPI #1 = IRQ 33) +pub const UART_IRQ: u32 = 33; + +/// VirtIO MMIO IRQ base (SPI #16 = IRQ 48) +/// QEMU virt assigns SPIs 48..79 to MMIO slots 0..31 +pub const VIRTIO_MMIO_IRQ_BASE: u32 = 48; + +// Spurious interrupt ID +const SPURIOUS_IRQ: u32 = 1023; + +// ========================================================= +// MMIO Helpers +// ========================================================= + +fn gicd_read(offset: usize) u32 { + const ptr: *volatile u32 = @ptrFromInt(GICD_BASE + offset); + return ptr.*; +} + +fn gicd_write(offset: usize, val: u32) void { + const ptr: *volatile u32 = @ptrFromInt(GICD_BASE + offset); + ptr.* = val; +} + +fn gicc_read(offset: usize) u32 { + const ptr: *volatile u32 = @ptrFromInt(GICC_BASE + offset); + return ptr.*; +} + +fn gicc_write(offset: usize, val: u32) void { + const ptr: *volatile u32 = @ptrFromInt(GICC_BASE + offset); + ptr.* = val; +} + +// ========================================================= +// Public API +// ========================================================= + +/// Initialize GICv2 distributor and CPU interface. +pub fn gic_init() void { + // 1. Disable distributor during setup + gicd_write(GICD_CTLR, 0); + + // 2. Set all SPIs to lowest priority (0xFF) and target CPU 0 + // PPIs (0-31) are banked per-CPU, handled separately + const typer = gicd_read(GICD_TYPER); + const it_lines = (typer & 0x1F) + 1; // Number of 32-IRQ groups + var i: usize = 1; // Skip group 0 (SGIs/PPIs - banked) + while (i < it_lines) : (i += 1) { + // Disable all SPIs + gicd_write(GICD_ICENABLER + i * 4, 0xFFFFFFFF); + // Set priority to 0xA0 (low but not lowest) + var j: usize = 0; + while (j < 8) : (j += 1) { + gicd_write(GICD_IPRIORITYR + (i * 32 + j * 4), 0xA0A0A0A0); + } + // Target CPU 0 for all SPIs + j = 0; + while (j < 8) : (j += 1) { + gicd_write(GICD_ITARGETSR + (i * 32 + j * 4), 0x01010101); + } + } + + // 3. Configure PPI priorities (group 0, banked) + // Timer IRQ 30: priority 0x20 (high) + const timer_prio_reg = GICD_IPRIORITYR + (TIMER_IRQ / 4) * 4; + const timer_prio_shift: u5 = @intCast((TIMER_IRQ % 4) * 8); + var prio_val = gicd_read(timer_prio_reg); + prio_val &= ~(@as(u32, 0xFF) << timer_prio_shift); + prio_val |= @as(u32, 0x20) << timer_prio_shift; + gicd_write(timer_prio_reg, prio_val); + + // 4. Enable distributor (Group 0 + Group 1) + gicd_write(GICD_CTLR, 0x3); + + // 5. Configure CPU interface + gicc_write(GICC_PMR, 0xFF); // Accept all priorities + gicc_write(GICC_CTLR, 0x1); // Enable CPU interface +} + +/// Enable a specific interrupt in the distributor. +pub fn gic_enable_irq(irq: u32) void { + const reg = GICD_ISENABLER + (irq / 32) * 4; + const bit: u5 = @intCast(irq % 32); + gicd_write(reg, @as(u32, 1) << bit); +} + +/// Disable a specific interrupt in the distributor. +pub fn gic_disable_irq(irq: u32) void { + const reg = GICD_ICENABLER + (irq / 32) * 4; + const bit: u5 = @intCast(irq % 32); + gicd_write(reg, @as(u32, 1) << bit); +} + +/// Acknowledge an interrupt (read IAR). Returns IRQ number or SPURIOUS_IRQ. +pub fn gic_claim() u32 { + return gicc_read(GICC_IAR) & 0x3FF; +} + +/// Signal end of interrupt processing. +pub fn gic_complete(irq: u32) void { + gicc_write(GICC_EOIR, irq); +} + +/// Check if a claimed IRQ is spurious. +pub fn is_spurious(irq: u32) bool { + return irq >= SPURIOUS_IRQ; +} + +/// Enable the NS Physical Timer interrupt (IRQ 30). +pub fn gic_enable_timer_irq() void { + gic_enable_irq(TIMER_IRQ); +} + +/// Enable a VirtIO MMIO slot interrupt in the GIC. +pub fn gic_enable_virtio_mmio_irq(slot: u32) void { + gic_enable_irq(VIRTIO_MMIO_IRQ_BASE + slot); +} diff --git a/hal/mm.zig b/hal/mm.zig index f64ce74..88c216c 100644 --- a/hal/mm.zig +++ b/hal/mm.zig @@ -205,8 +205,10 @@ pub fn create_worker_map(stack_base: u64, stack_size: u64, packet_addr: u64, phy try map_range(root, 0x40000000, 0x40000000, 0x10000000, PTE_R | PTE_W); // PCIe MMIO try map_range(root, 0x20000000, 0x20000000, 0x10000, PTE_R | PTE_W); // PTY Slave - // 4. Overlap stack with user access - try map_range(root, stack_base, stack_base, stack_size, PTE_R | PTE_W | PTE_U); + // 4. Overlap stack with user access (Optional) + if (stack_base != 0) { + try map_range(root, stack_base, stack_base, stack_size, PTE_R | PTE_W | PTE_U); + } // 5. Shared SysTable & Rings & User Slab (0x83000000) - Map 256KB (64 pages; covers up to 0x40000) var j: u64 = 0; diff --git a/hal/ontology.zig b/hal/ontology.zig index 1e02185..dbaaecd 100644 --- a/hal/ontology.zig +++ b/hal/ontology.zig @@ -194,10 +194,12 @@ pub export fn stl_init() void { stl_initialized = true; } -/// Get current timestamp (placeholder - will be replaced by HAL timer) +/// Sovereign timer — canonical time source for all kernel timestamps +extern fn rumpk_timer_now_ns() u64; + +/// Get current timestamp in nanoseconds since boot fn get_timestamp_ns() u64 { - // TODO: Integrate with HAL timer - return 0; + return rumpk_timer_now_ns(); } /// Emit event to STL (C ABI) diff --git a/hal/stubs.zig b/hal/stubs.zig index 567765b..1718e0e 100644 --- a/hal/stubs.zig +++ b/hal/stubs.zig @@ -15,6 +15,9 @@ const uart = @import("uart.zig"); +// Sovereign timer — canonical time source for the entire kernel +extern fn rumpk_timer_now_ns() u64; + // ========================================================= // Heap Stubs (Bump Allocator with Block Headers) // ========================================================= @@ -137,13 +140,9 @@ export fn calloc(nmemb: usize, size: usize) ?*anyopaque { // ========================================================= export fn get_ticks() u32 { - var time_val: u64 = 0; - asm volatile ("rdtime %[ret]" - : [ret] "=r" (time_val), - ); - // QEMU 'virt' RISC-V timebase is 10MHz (10,000,000 Hz). - // Convert to milliseconds: val / 10,000. - return @truncate(time_val / 10000); + // Delegate to sovereign timer — single source of truth for all time + const ns = rumpk_timer_now_ns(); + return @truncate(ns / 1_000_000); // ns → ms } // export fn rumpk_timer_set_ns(ns: u64) void { @@ -160,10 +159,10 @@ export fn nexshell_main() void { } extern fn k_handle_syscall(nr: usize, a0: usize, a1: usize, a2: usize) usize; -export fn exit(code: c_int) noreturn { - _ = code; - while (true) asm volatile ("wfi"); -} +// export fn exit(code: c_int) noreturn { +// _ = code; +// while (true) asm volatile ("wfi"); +// } // ========================================================= // Atomic Stubs (To resolve linker errors with libcompiler_rt) diff --git a/hal/stubs_user.zig b/hal/stubs_user.zig index 43d111a..cda92d3 100644 --- a/hal/stubs_user.zig +++ b/hal/stubs_user.zig @@ -13,7 +13,7 @@ //! DECISION(Alloc): Bump allocator chosen for simplicity and determinism. //! Memory is never reclaimed; system reboots to reset. -const uart = @import("uart.zig"); +// const uart = @import("uart.zig"); // ========================================================= // Heap Stubs (Bump Allocator with Block Headers) @@ -27,11 +27,13 @@ var heap_idx: usize = 0; var heap_init_done: bool = false; export fn debug_print(s: [*]const u8, len: usize) void { - uart.print(s[0..len]); + _ = s; + _ = len; + // TODO: Use syscall for userland debug printing } export fn kprint_hex(value: u64) void { - uart.print_hex(value); + _ = value; } // Header structure (64 bytes aligned to match LwIP MEM_ALIGNMENT) @@ -45,9 +47,9 @@ export fn malloc(size: usize) ?*anyopaque { if (!heap_init_done) { if (heap_idx != 0) { - uart.print("[Alloc] WARNING: BSS NOT ZEROED! heap_idx: "); - uart.print_hex(heap_idx); - uart.print("\n"); + // uart.print("[Alloc] WARNING: BSS NOT ZEROED! heap_idx: "); + // uart.print_hex(heap_idx); + // uart.print("\n"); heap_idx = 0; } heap_init_done = true; @@ -58,11 +60,11 @@ export fn malloc(size: usize) ?*anyopaque { const aligned_idx = (heap_idx + align_mask) & ~align_mask; if (aligned_idx + total_needed > heap.len) { - uart.print("[Alloc] OOM! Size: "); - uart.print_hex(size); - uart.print(" Used: "); - uart.print_hex(heap_idx); - uart.print("\n"); + // uart.print("[Alloc] OOM! Size: "); + // uart.print_hex(size); + // uart.print(" Used: "); + // uart.print_hex(heap_idx); + // uart.print("\n"); return null; } @@ -137,3 +139,29 @@ export fn calloc(nmemb: usize, size: usize) ?*anyopaque { export fn get_ticks() u32 { return 0; // TODO: Implement real timer } + +// export fn strlen(s: [*]const u8) usize { +// var i: usize = 0; +// while (s[i] != 0) : (i += 1) {} +// return i; +// } + +// export fn fwrite(ptr: ?*anyopaque, size: usize, nmemb: usize, stream: ?*anyopaque) usize { +// _ = ptr; +// _ = size; +// _ = nmemb; +// _ = stream; +// return 0; +// } + +// export fn fflush(stream: ?*anyopaque) c_int { +// _ = stream; +// return 0; +// } + +// export fn write(fd: c_int, buf: ?*anyopaque, count: usize) isize { +// _ = fd; +// _ = buf; +// _ = count; +// return 0; +// } diff --git a/hal/uart.zig b/hal/uart.zig index 9df642b..20fd699 100644 --- a/hal/uart.zig +++ b/hal/uart.zig @@ -34,9 +34,19 @@ pub const NS16550A_LCR: usize = 0x03; // Line Control Register // Input logic moved to uart_input.zig +// PL011 Additional Registers +pub const PL011_IBRD: usize = 0x24; // Integer Baud Rate Divisor +pub const PL011_FBRD: usize = 0x28; // Fractional Baud Rate Divisor +pub const PL011_LCR_H: usize = 0x2C; // Line Control +pub const PL011_CR: usize = 0x30; // Control +pub const PL011_IMSC: usize = 0x38; // Interrupt Mask Set/Clear +pub const PL011_ICR: usize = 0x44; // Interrupt Clear +pub const PL011_RXFE: u32 = 1 << 4; // Receive FIFO Empty + pub fn init() void { switch (builtin.cpu.arch) { .riscv64 => init_riscv(), + .aarch64 => init_aarch64(), else => {}, } } @@ -107,6 +117,78 @@ pub fn init_riscv() void { // uart_input.poll_input(); // We cannot call this here safely without dep } +pub fn init_aarch64() void { + const base = PL011_BASE; + + // 1. Disable UART during setup + const cr: *volatile u32 = @ptrFromInt(base + PL011_CR); + cr.* = 0; + + // 2. Clear all pending interrupts + const icr: *volatile u32 = @ptrFromInt(base + PL011_ICR); + icr.* = 0x7FF; + + // 3. Set baud rate (115200 @ 24MHz QEMU clock) + // IBRD = 24000000 / (16 * 115200) = 13 + // FBRD = ((0.0208... * 64) + 0.5) = 1 + const ibrd: *volatile u32 = @ptrFromInt(base + PL011_IBRD); + const fbrd: *volatile u32 = @ptrFromInt(base + PL011_FBRD); + ibrd.* = 13; + fbrd.* = 1; + + // 4. Line Control: 8N1, FIFO enable + const lcr_h: *volatile u32 = @ptrFromInt(base + PL011_LCR_H); + lcr_h.* = (0x3 << 5) | (1 << 4); // WLEN=8bit, FEN=1 + + // 5. Enable receive interrupt + const imsc: *volatile u32 = @ptrFromInt(base + PL011_IMSC); + imsc.* = (1 << 4); // RXIM: Receive interrupt mask + + // 6. Enable UART: TXE + RXE + UARTEN + cr.* = (1 << 8) | (1 << 9) | (1 << 0); // TXE | RXE | UARTEN + + // --- LOOPBACK TEST --- + // PL011 has loopback via CR bit 7 (LBE) + cr.* = cr.* | (1 << 7); // Enable loopback + + // Write test byte + const dr: *volatile u32 = @ptrFromInt(base + PL011_DR); + const fr: *volatile u32 = @ptrFromInt(base + PL011_FR); + + // Wait for TX not full + while ((fr.* & PL011_TXFF) != 0) {} + dr.* = 0xA5; + + // Wait for RX not empty + var timeout: usize = 1000000; + while ((fr.* & PL011_RXFE) != 0 and timeout > 0) { + timeout -= 1; + } + + var passed = false; + var reason: []const u8 = "Timeout"; + + if ((fr.* & PL011_RXFE) == 0) { + const val: u8 = @truncate(dr.*); + if (val == 0xA5) { + passed = true; + } else { + reason = "Data Mismatch"; + } + } + + // Disable loopback + cr.* = cr.* & ~@as(u32, 1 << 7); + + if (passed) { + write_bytes("[UART] Loopback Test: PASS\n"); + } else { + write_bytes("[UART] Loopback Test: FAIL ("); + write_bytes(reason); + write_bytes(")\n"); + } +} + fn write_char_arm64(c: u8) void { const dr: *volatile u32 = @ptrFromInt(PL011_BASE + PL011_DR); const fr: *volatile u32 = @ptrFromInt(PL011_BASE + PL011_FR); @@ -152,6 +234,13 @@ pub fn read_direct() ?u8 { return thr.*; } }, + .aarch64 => { + const dr: *volatile u32 = @ptrFromInt(PL011_BASE + PL011_DR); + const fr: *volatile u32 = @ptrFromInt(PL011_BASE + PL011_FR); + if ((fr.* & PL011_RXFE) == 0) { + return @truncate(dr.*); + } + }, else => {}, } return null; @@ -163,6 +252,11 @@ pub fn get_lsr() u8 { const lsr: *volatile u8 = @ptrFromInt(NS16550A_BASE + NS16550A_LSR); return lsr.*; }, + .aarch64 => { + // Return PL011 flags register (low byte) + const fr: *volatile u32 = @ptrFromInt(PL011_BASE + PL011_FR); + return @truncate(fr.*); + }, else => return 0, } } diff --git a/hal/uart_input.zig b/hal/uart_input.zig index 05aef43..da7667f 100644 --- a/hal/uart_input.zig +++ b/hal/uart_input.zig @@ -20,6 +20,7 @@ pub fn poll_input() void { // Only Kernel uses this const Kernel = struct { extern fn ion_push_stdin(ptr: [*]const u8, len: usize) void; + extern fn kprint(s: [*]const u8) void; }; switch (builtin.cpu.arch) { @@ -34,6 +35,9 @@ pub fn poll_input() void { const byte = thr.*; const byte_arr = [1]u8{byte}; + // DEBUG: Trace hardware read + Kernel.kprint("[HW Read]\n"); + // Forward to Kernel Input Channel Kernel.ion_push_stdin(&byte_arr, 1); diff --git a/hal/virtio_block.zig b/hal/virtio_block.zig index 2b86e74..8211e79 100644 --- a/hal/virtio_block.zig +++ b/hal/virtio_block.zig @@ -13,8 +13,14 @@ //! the request. Uses bounce-buffers to guarantee alignment. const std = @import("std"); +const builtin = @import("builtin"); const uart = @import("uart.zig"); -const pci = @import("virtio_pci.zig"); + +// Comptime transport switch: PCI on RISC-V, MMIO on ARM64 +const transport_mod = if (builtin.cpu.arch == .aarch64) + @import("virtio_mmio.zig") +else + @import("virtio_pci.zig"); // External C/Zig stubs extern fn malloc(size: usize) ?*anyopaque; @@ -46,13 +52,43 @@ pub fn init() void { } pub const VirtioBlkDriver = struct { - transport: pci.VirtioTransport, + transport: transport_mod.VirtioTransport, v_desc: [*]volatile VirtioDesc, v_avail: *volatile VirtioAvail, v_used: *volatile VirtioUsed, queue_size: u16, pub fn probe() ?VirtioBlkDriver { + if (builtin.cpu.arch == .aarch64) { + return probe_mmio(); + } else { + return probe_pci(); + } + } + + fn probe_mmio() ?VirtioBlkDriver { + const mmio = @import("virtio_mmio.zig"); + const base = mmio.find_device(2) orelse { // device_id=2 is block + uart.print("[VirtIO] No VirtIO-Block MMIO device found\n"); + return null; + }; + uart.print("[VirtIO] Found VirtIO-Block at MMIO 0x"); + uart.print_hex(base); + uart.print("\n"); + var self = VirtioBlkDriver{ + .transport = transport_mod.VirtioTransport.init(base), + .v_desc = undefined, + .v_avail = undefined, + .v_used = undefined, + .queue_size = 0, + }; + if (self.init_device()) { + return self; + } + return null; + } + + fn probe_pci() ?VirtioBlkDriver { const PCI_ECAM_BASE: usize = 0x30000000; const bus: u8 = 0; const func: u8 = 0; @@ -69,7 +105,7 @@ pub const VirtioBlkDriver = struct { uart.print_hex(i); uart.print(".0\n"); var self = VirtioBlkDriver{ - .transport = pci.VirtioTransport.init(addr), + .transport = transport_mod.VirtioTransport.init(addr), .v_desc = undefined, .v_avail = undefined, .v_used = undefined, @@ -87,29 +123,56 @@ pub const VirtioBlkDriver = struct { if (!self.transport.probe()) return false; self.transport.reset(); - self.transport.add_status(1); - self.transport.add_status(2); + self.transport.add_status(1); // ACKNOWLEDGE + self.transport.add_status(2); // DRIVER + + // Feature negotiation + const dev_features = self.transport.get_device_features(); + _ = dev_features; + // Accept no special features for block — just basic operation + self.transport.set_driver_features(0); + transport_mod.io_barrier(); + // FEATURES_OK only on modern (v2) transport + if (self.transport.is_modern) { + self.transport.add_status(8); // FEATURES_OK + transport_mod.io_barrier(); + } self.transport.select_queue(0); - const count = self.transport.get_queue_size(); + const max_count = self.transport.get_queue_size(); + // Cap queue size for memory efficiency + const MAX_BLK_QUEUE: u16 = 128; + const count = if (max_count > MAX_BLK_QUEUE) MAX_BLK_QUEUE else max_count; // [Desc] [Avail] [Used] (Simplified layout) const total = (count * 16) + (6 + count * 2) + 4096 + (6 + count * 8); const raw_ptr = malloc(total + 4096) orelse return false; const aligned = (@intFromPtr(raw_ptr) + 4095) & ~@as(usize, 4095); + // Zero out queue memory to ensure clean state + const byte_ptr: [*]u8 = @ptrFromInt(aligned); + for (0..total) |i| { + byte_ptr[i] = 0; + } + self.v_desc = @ptrFromInt(aligned); self.v_avail = @ptrFromInt(aligned + (count * 16)); self.v_used = @ptrFromInt(aligned + (count * 16) + (6 + count * 2) + 4096); self.queue_size = count; + // Ensure avail/used rings start clean + self.v_avail.flags = 0; + self.v_avail.idx = 0; + self.v_used.flags = 0; + if (self.transport.is_modern) { self.transport.setup_modern_queue(aligned, aligned + (count * 16), @intFromPtr(self.v_used)); } else { + self.transport.set_queue_size(count); self.transport.setup_legacy_queue(@intCast(aligned >> 12)); } - self.transport.add_status(4); + self.transport.add_status(4); // DRIVER_OK global_blk = self.*; uart.print("[VirtIO-Blk] Device Ready. Queue Size: "); @@ -151,15 +214,26 @@ pub const VirtioBlkDriver = struct { // Submit to Avail Ring const ring = @as([*]volatile u16, @ptrFromInt(@intFromPtr(self.v_avail) + 4)); ring[self.v_avail.idx % self.queue_size] = 0; // Head of chain - asm volatile ("fence w, w" ::: .{ .memory = true }); + const expected_used = self.v_used.idx +% 1; + transport_mod.io_barrier(); self.v_avail.idx +%= 1; - asm volatile ("fence w, w" ::: .{ .memory = true }); + transport_mod.io_barrier(); self.transport.notify(0); - // Wait for device (Polling) - while (self.v_used.idx == 0) { - asm volatile ("nop"); + // Wait for device (Polling — wait until used ring advances) + var timeout: usize = 0; + while (self.v_used.idx != expected_used) { + transport_mod.io_barrier(); + timeout += 1; + if (timeout > 100_000_000) { + uart.print("[VirtIO-Blk] READ TIMEOUT! used.idx="); + uart.print_hex(self.v_used.idx); + uart.print(" expected="); + uart.print_hex(expected_used); + uart.print("\n"); + return error.DiskError; + } } if (status != 0) return error.DiskError; @@ -190,14 +264,22 @@ pub const VirtioBlkDriver = struct { const ring = @as([*]volatile u16, @ptrFromInt(@intFromPtr(self.v_avail) + 4)); ring[self.v_avail.idx % self.queue_size] = 3; - asm volatile ("fence w, w" ::: .{ .memory = true }); + const expected_used = self.v_used.idx +% 1; + transport_mod.io_barrier(); self.v_avail.idx +%= 1; - asm volatile ("fence w, w" ::: .{ .memory = true }); + transport_mod.io_barrier(); self.transport.notify(0); - while (status == 0xFF) { - asm volatile ("nop"); + // Wait for device (Polling — wait until used ring advances) + var timeout: usize = 0; + while (self.v_used.idx != expected_used) { + transport_mod.io_barrier(); + timeout += 1; + if (timeout > 100_000_000) { + uart.print("[VirtIO-Blk] WRITE TIMEOUT!\n"); + return error.DiskError; + } } if (status != 0) return error.DiskError; diff --git a/hal/virtio_mmio.zig b/hal/virtio_mmio.zig new file mode 100644 index 0000000..f52cbc7 --- /dev/null +++ b/hal/virtio_mmio.zig @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: LCL-1.0 +// Copyright (c) 2026 Markus Maiwald +// Stewardship: Self Sovereign Society Foundation +// +// This file is part of the Nexus Commonwealth. +// See legal/LICENSE_COMMONWEALTH.md for license terms. + +//! Rumpk HAL: VirtIO MMIO Transport Layer (ARM64) +//! +//! Provides the same VirtioTransport API as virtio_pci.zig but for MMIO-based +//! VirtIO devices as found on QEMU aarch64 virt machine. +//! +//! QEMU virt MMIO layout: 32 slots starting at 0x0a000000, stride 0x200. +//! Each slot triggers GIC SPI (IRQ 48 + slot_index). +//! +//! Supports both legacy (v1) and modern (v2) MMIO transport. +//! +//! SAFETY: All hardware registers accessed via volatile pointers. + +const std = @import("std"); +const builtin = @import("builtin"); +const uart = @import("uart.zig"); + +// ========================================================= +// VirtIO MMIO Register Offsets (spec §4.2.2) +// ========================================================= + +const VIRTIO_MMIO_MAGIC_VALUE = 0x000; +const VIRTIO_MMIO_VERSION = 0x004; +const VIRTIO_MMIO_DEVICE_ID = 0x008; +const VIRTIO_MMIO_VENDOR_ID = 0x00C; +const VIRTIO_MMIO_DEVICE_FEATURES = 0x010; +const VIRTIO_MMIO_DEVICE_FEATURES_SEL = 0x014; +const VIRTIO_MMIO_DRIVER_FEATURES = 0x020; +const VIRTIO_MMIO_DRIVER_FEATURES_SEL = 0x024; +const VIRTIO_MMIO_QUEUE_SEL = 0x030; +const VIRTIO_MMIO_QUEUE_NUM_MAX = 0x034; +const VIRTIO_MMIO_QUEUE_NUM = 0x038; +const VIRTIO_MMIO_QUEUE_ALIGN = 0x03C; +const VIRTIO_MMIO_QUEUE_PFN = 0x040; +const VIRTIO_MMIO_QUEUE_READY = 0x044; +const VIRTIO_MMIO_QUEUE_NOTIFY = 0x050; +const VIRTIO_MMIO_INTERRUPT_STATUS = 0x060; +const VIRTIO_MMIO_INTERRUPT_ACK = 0x064; +const VIRTIO_MMIO_STATUS = 0x070; +const VIRTIO_MMIO_QUEUE_DESC_LOW = 0x080; +const VIRTIO_MMIO_QUEUE_DESC_HIGH = 0x084; +const VIRTIO_MMIO_QUEUE_AVAIL_LOW = 0x090; +const VIRTIO_MMIO_QUEUE_AVAIL_HIGH = 0x094; +const VIRTIO_MMIO_QUEUE_USED_LOW = 0x0A0; +const VIRTIO_MMIO_QUEUE_USED_HIGH = 0x0A4; +const VIRTIO_MMIO_CONFIG = 0x100; // Device-specific config starts here + +// VirtIO magic value: "virt" in little-endian +const VIRTIO_MAGIC: u32 = 0x74726976; + +// ========================================================= +// QEMU virt MMIO Topology +// ========================================================= + +const MMIO_BASE: usize = 0x0a000000; +const MMIO_STRIDE: usize = 0x200; +const MMIO_SLOT_COUNT: usize = 32; +const MMIO_IRQ_BASE: u32 = 48; // GIC SPI base for VirtIO MMIO + +// ========================================================= +// MMIO Read/Write Helpers +// ========================================================= + +fn mmio_read(base: usize, offset: usize) u32 { + const ptr: *volatile u32 = @ptrFromInt(base + offset); + return ptr.*; +} + +fn mmio_write(base: usize, offset: usize, val: u32) void { + const ptr: *volatile u32 = @ptrFromInt(base + offset); + ptr.* = val; +} + +fn mmio_read_u8(base: usize, offset: usize) u8 { + const ptr: *volatile u8 = @ptrFromInt(base + offset); + return ptr.*; +} + +// ========================================================= +// Arch-safe memory barrier +// ========================================================= + +pub inline fn io_barrier() void { + switch (builtin.cpu.arch) { + .aarch64 => asm volatile ("dmb sy" ::: .{ .memory = true }), + .riscv64 => asm volatile ("fence" ::: .{ .memory = true }), + else => @compileError("unsupported arch"), + } +} + +// ========================================================= +// VirtIO MMIO Transport (same API surface as PCI transport) +// ========================================================= + +pub const VirtioTransport = struct { + base_addr: usize, + is_modern: bool, + version: u32, + + // Legacy compatibility fields (match PCI transport shape) + legacy_bar: usize, + + // Modern interface placeholders (unused for MMIO but present for API compat) + common_cfg: ?*volatile anyopaque, + notify_cfg: ?usize, + notify_off_multiplier: u32, + isr_cfg: ?*volatile u8, + device_cfg: ?*volatile u8, + + pub fn init(mmio_base: usize) VirtioTransport { + return .{ + .base_addr = mmio_base, + .is_modern = false, + .version = 0, + .legacy_bar = 0, + .common_cfg = null, + .notify_cfg = null, + .notify_off_multiplier = 0, + .isr_cfg = null, + .device_cfg = null, + }; + } + + pub fn probe(self: *VirtioTransport) bool { + const magic = mmio_read(self.base_addr, VIRTIO_MMIO_MAGIC_VALUE); + if (magic != VIRTIO_MAGIC) return false; + + self.version = mmio_read(self.base_addr, VIRTIO_MMIO_VERSION); + if (self.version != 1 and self.version != 2) return false; + + const device_id = mmio_read(self.base_addr, VIRTIO_MMIO_DEVICE_ID); + if (device_id == 0) return false; // No device at this slot + + self.is_modern = (self.version == 2); + + uart.print("[VirtIO-MMIO] Probed 0x"); + uart.print_hex(self.base_addr); + uart.print(" Ver="); + uart.print_hex(self.version); + uart.print(" DevID="); + uart.print_hex(device_id); + uart.print("\n"); + + return true; + } + + pub fn reset(self: *VirtioTransport) void { + self.set_status(0); + // After reset, wait for device to reinitialize (spec §2.1.1) + io_barrier(); + } + + pub fn get_status(self: *VirtioTransport) u8 { + return @truncate(mmio_read(self.base_addr, VIRTIO_MMIO_STATUS)); + } + + pub fn set_status(self: *VirtioTransport, status: u8) void { + mmio_write(self.base_addr, VIRTIO_MMIO_STATUS, @as(u32, status)); + } + + pub fn add_status(self: *VirtioTransport, status: u8) void { + self.set_status(self.get_status() | status); + } + + pub fn select_queue(self: *VirtioTransport, idx: u16) void { + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_SEL, @as(u32, idx)); + } + + pub fn get_queue_size(self: *VirtioTransport) u16 { + return @truncate(mmio_read(self.base_addr, VIRTIO_MMIO_QUEUE_NUM_MAX)); + } + + pub fn set_queue_size(self: *VirtioTransport, size: u16) void { + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_NUM, @as(u32, size)); + } + + pub fn setup_legacy_queue(self: *VirtioTransport, pfn: u32) void { + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_ALIGN, 4096); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_PFN, pfn); + } + + pub fn setup_modern_queue(self: *VirtioTransport, desc: u64, avail: u64, used: u64) void { + // Set queue size first + const max_size = mmio_read(self.base_addr, VIRTIO_MMIO_QUEUE_NUM_MAX); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_NUM, max_size); + + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_DESC_LOW, @truncate(desc)); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_DESC_HIGH, @truncate(desc >> 32)); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_AVAIL_LOW, @truncate(avail)); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, @truncate(avail >> 32)); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_USED_LOW, @truncate(used)); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_USED_HIGH, @truncate(used >> 32)); + + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_READY, 1); + } + + pub fn notify(self: *VirtioTransport, queue_idx: u16) void { + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_NOTIFY, @as(u32, queue_idx)); + } + + // ========================================================= + // Unified Accessor API (matches PCI transport extensions) + // ========================================================= + + pub fn get_device_features(self: *VirtioTransport) u64 { + mmio_write(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES_SEL, 0); + io_barrier(); + const low: u64 = mmio_read(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES); + + mmio_write(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES_SEL, 1); + io_barrier(); + const high: u64 = mmio_read(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES); + + return (high << 32) | low; + } + + pub fn set_driver_features(self: *VirtioTransport, features: u64) void { + mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0); + mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES, @truncate(features)); + io_barrier(); + mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1); + mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES, @truncate(features >> 32)); + io_barrier(); + } + + pub fn get_device_config_byte(self: *VirtioTransport, offset: usize) u8 { + return mmio_read_u8(self.base_addr, VIRTIO_MMIO_CONFIG + offset); + } + + pub fn ack_interrupt(self: *VirtioTransport) u32 { + const status = mmio_read(self.base_addr, VIRTIO_MMIO_INTERRUPT_STATUS); + mmio_write(self.base_addr, VIRTIO_MMIO_INTERRUPT_ACK, status); + return status; + } +}; + +// ========================================================= +// Device Discovery +// ========================================================= + +/// Scan MMIO slots for a VirtIO device with the given device ID. +/// Returns MMIO base address or null if not found. +pub fn find_device(device_id: u32) ?usize { + var slot: usize = 0; + while (slot < MMIO_SLOT_COUNT) : (slot += 1) { + const base = MMIO_BASE + (slot * MMIO_STRIDE); + const magic = mmio_read(base, VIRTIO_MMIO_MAGIC_VALUE); + if (magic != VIRTIO_MAGIC) continue; + + const dev_id = mmio_read(base, VIRTIO_MMIO_DEVICE_ID); + if (dev_id == device_id) { + return base; + } + } + return null; +} + +/// Get the GIC SPI number for a given MMIO slot base address. +pub fn slot_irq(base: usize) u32 { + const slot = (base - MMIO_BASE) / MMIO_STRIDE; + return MMIO_IRQ_BASE + @as(u32, @intCast(slot)); +} diff --git a/hal/virtio_net.zig b/hal/virtio_net.zig index 0efe020..c739377 100644 --- a/hal/virtio_net.zig +++ b/hal/virtio_net.zig @@ -14,8 +14,14 @@ //! to ensure correct synchronization with the virtual device. const std = @import("std"); +const builtin = @import("builtin"); const uart = @import("uart.zig"); -const pci = @import("virtio_pci.zig"); + +// Comptime transport switch: PCI on RISC-V, MMIO on ARM64 +const transport_mod = if (builtin.cpu.arch == .aarch64) + @import("virtio_mmio.zig") +else + @import("virtio_pci.zig"); // VirtIO Feature Bits const VIRTIO_F_VERSION_1 = 32; @@ -117,47 +123,24 @@ pub export fn rumpk_net_init() void { } pub const VirtioNetDriver = struct { - transport: pci.VirtioTransport, + transport: transport_mod.VirtioTransport, irq: u32, rx_queue: ?*Virtqueue = null, tx_queue: ?*Virtqueue = null, pub fn get_mac(self: *VirtioNetDriver, out: [*]u8) void { - uart.print("[VirtIO-Net] Reading MAC from device_cfg...\n"); - if (self.transport.is_modern) { - // Use device_cfg directly - this is the VirtIO-Net specific config - if (self.transport.device_cfg) |cfg| { - const ptr: [*]volatile u8 = @ptrCast(cfg); - uart.print(" DeviceCfg at: "); - uart.print_hex(@intFromPtr(cfg)); - uart.print("\n MAC bytes: "); - - for (0..6) |i| { - out[i] = ptr[i]; - uart.print_hex8(ptr[i]); - if (i < 5) uart.print(":"); - } - uart.print("\n"); - } else { - uart.print(" ERROR: device_cfg is null!\n"); - // Fallback to zeros - for (0..6) |i| { - out[i] = 0; - } - } - } else { - // Legacy - // Device Config starts at offset 20. - const base = self.transport.legacy_bar + 20; - for (0..6) |i| { - out[i] = @as(*volatile u8, @ptrFromInt(base + i)).*; - } + uart.print("[VirtIO-Net] Reading MAC from device config...\n"); + for (0..6) |i| { + out[i] = self.transport.get_device_config_byte(i); + uart.print_hex8(out[i]); + if (i < 5) uart.print(":"); } + uart.print("\n"); } pub fn init(base: usize, irq_num: u32) VirtioNetDriver { return .{ - .transport = pci.VirtioTransport.init(base), + .transport = transport_mod.VirtioTransport.init(base), .irq = irq_num, .rx_queue = null, .tx_queue = null, @@ -165,6 +148,32 @@ pub const VirtioNetDriver = struct { } pub fn probe() ?VirtioNetDriver { + if (builtin.cpu.arch == .aarch64) { + return probe_mmio(); + } else { + return probe_pci(); + } + } + + fn probe_mmio() ?VirtioNetDriver { + uart.print("[VirtIO] Probing MMIO for networking device...\n"); + const mmio = @import("virtio_mmio.zig"); + const base = mmio.find_device(1) orelse { // device_id=1 is net + uart.print("[VirtIO] No VirtIO-Net MMIO device found\n"); + return null; + }; + uart.print("[VirtIO] Found VirtIO-Net at MMIO 0x"); + uart.print_hex(base); + uart.print("\n"); + const irq = mmio.slot_irq(base); + var self = VirtioNetDriver.init(base, irq); + if (self.init_device()) { + return self; + } + return null; + } + + fn probe_pci() ?VirtioNetDriver { uart.print("[VirtIO] Probing PCI for networking device...\n"); const PCI_ECAM_BASE: usize = 0x30000000; const bus: u8 = 0; @@ -213,52 +222,22 @@ pub const VirtioNetDriver = struct { self.transport.add_status(VIRTIO_CONFIG_S_ACKNOWLEDGE); self.transport.add_status(VIRTIO_CONFIG_S_DRIVER); - // 4. Feature Negotiation - if (self.transport.is_modern) { + // 4. Feature Negotiation (unified across PCI and MMIO) + { uart.print("[VirtIO] Starting feature negotiation...\n"); - - if (self.transport.common_cfg == null) { - uart.print("[VirtIO] ERROR: common_cfg is null!\n"); - return false; - } - - const cfg = self.transport.common_cfg.?; - uart.print("[VirtIO] common_cfg addr: "); - uart.print_hex(@intFromPtr(cfg)); - uart.print("\n"); - - uart.print("[VirtIO] Reading device features...\n"); - // Read Device Features (Page 0) - cfg.device_feature_select = 0; - asm volatile ("fence" ::: .{ .memory = true }); - const f_low = cfg.device_feature; - - // Read Device Features (Page 1) - cfg.device_feature_select = 1; - asm volatile ("fence" ::: .{ .memory = true }); - const f_high = cfg.device_feature; - + const dev_features = self.transport.get_device_features(); uart.print("[VirtIO] Device Features: "); - uart.print_hex(f_low); - uart.print(" "); - uart.print_hex(f_high); + uart.print_hex(dev_features); uart.print("\n"); // Accept VERSION_1 (Modern) and MAC - const accept_low: u32 = (1 << VIRTIO_NET_F_MAC); - const accept_high: u32 = (1 << (VIRTIO_F_VERSION_1 - 32)); + const accept: u64 = (1 << VIRTIO_NET_F_MAC) | + (@as(u64, 1) << VIRTIO_F_VERSION_1); + self.transport.set_driver_features(accept); + transport_mod.io_barrier(); - uart.print("[VirtIO] Writing driver features...\n"); - cfg.driver_feature_select = 0; - cfg.driver_feature = accept_low; - asm volatile ("fence" ::: .{ .memory = true }); - cfg.driver_feature_select = 1; - cfg.driver_feature = accept_high; - asm volatile ("fence" ::: .{ .memory = true }); - - uart.print("[VirtIO] Checking feature negotiation...\n"); self.transport.add_status(VIRTIO_CONFIG_S_FEATURES_OK); - asm volatile ("fence" ::: .{ .memory = true }); + transport_mod.io_barrier(); if ((self.transport.get_status() & VIRTIO_CONFIG_S_FEATURES_OK) == 0) { uart.print("[VirtIO] Feature negotiation failed!\n"); return false; @@ -267,10 +246,15 @@ pub const VirtioNetDriver = struct { } // 5. Setup RX Queue (0) self.transport.select_queue(0); - const rx_count = self.transport.get_queue_size(); + const rx_max = self.transport.get_queue_size(); + // Cap queue size to avoid ION pool exhaustion (MMIO v1 reports 1024) + const MAX_QUEUE: u16 = 256; + const rx_count = if (rx_max > MAX_QUEUE) MAX_QUEUE else rx_max; uart.print("[VirtIO] RX Queue Size: "); uart.print_hex(rx_count); - uart.print("\n"); + uart.print(" (max: "); + uart.print_hex(rx_max); + uart.print(")\n"); if (rx_count == 0 or rx_count == 0xFFFF) { uart.print("[VirtIO] Invalid RX Queue Size. Aborting.\n"); @@ -288,10 +272,13 @@ pub const VirtioNetDriver = struct { // 6. Setup TX Queue (1) self.transport.select_queue(1); - const tx_count = self.transport.get_queue_size(); + const tx_max = self.transport.get_queue_size(); + const tx_count = if (tx_max > MAX_QUEUE) MAX_QUEUE else tx_max; uart.print("[VirtIO] TX Queue Size: "); uart.print_hex(tx_count); - uart.print("\n"); + uart.print(" (max: "); + uart.print_hex(tx_max); + uart.print(")\n"); if (tx_count == 0 or tx_count == 0xFFFF) { uart.print("[VirtIO] Invalid TX Queue Size. Aborting.\n"); @@ -392,11 +379,11 @@ pub const VirtioNetDriver = struct { q_ptr.avail.flags = 0; q_ptr.used.flags = 0; - asm volatile ("fence w, w" ::: .{ .memory = true }); + transport_mod.io_barrier(); if (is_rx) { q_ptr.avail.idx = count; - asm volatile ("fence w, w" ::: .{ .memory = true }); + transport_mod.io_barrier(); } const phys_addr = aligned_addr; @@ -404,6 +391,7 @@ pub const VirtioNetDriver = struct { if (self.transport.is_modern) { self.transport.setup_modern_queue(phys_addr, phys_addr + desc_size, phys_addr + used_offset); } else { + self.transport.set_queue_size(count); const pfn = @as(u32, @intCast(phys_addr >> 12)); self.transport.setup_legacy_queue(pfn); } @@ -413,7 +401,7 @@ pub const VirtioNetDriver = struct { } pub fn rx_poll(self: *VirtioNetDriver, q: *Virtqueue) void { - asm volatile ("fence" ::: .{ .memory = true }); + transport_mod.io_barrier(); const used = q.used; const hw_idx = used.idx; @@ -473,7 +461,7 @@ pub const VirtioNetDriver = struct { q.desc[desc_idx].addr = new_phys; q.ids[desc_idx] = new_id; - asm volatile ("fence" ::: .{ .memory = true }); + transport_mod.io_barrier(); avail_ring[q.avail.idx % q.num] = @intCast(desc_idx); q.avail.idx +%= 1; @@ -486,14 +474,14 @@ pub const VirtioNetDriver = struct { } if (replenished) { - asm volatile ("fence" ::: .{ .memory = true }); + transport_mod.io_barrier(); self.transport.notify(0); } } pub fn tx_poll(self: *VirtioNetDriver, q: *Virtqueue) void { _ = self; - asm volatile ("fence" ::: .{ .memory = true }); + transport_mod.io_barrier(); const used = q.used; const used_idx = used.idx; const used_ring = get_used_ring(used); @@ -528,11 +516,11 @@ pub const VirtioNetDriver = struct { q.ids[idx] = slab_id; - asm volatile ("fence" ::: .{ .memory = true }); + transport_mod.io_barrier(); avail_ring[idx] = @intCast(idx); - asm volatile ("fence" ::: .{ .memory = true }); + transport_mod.io_barrier(); q.avail.idx +%= 1; - asm volatile ("fence" ::: .{ .memory = true }); + transport_mod.io_barrier(); self.transport.notify(1); uart.print("[VirtIO TX-Slab] Sent "); @@ -579,11 +567,11 @@ pub const VirtioNetDriver = struct { desc.len = @intCast(header_len + copy_len); desc.flags = 0; - asm volatile ("fence" ::: .{ .memory = true }); + transport_mod.io_barrier(); avail_ring[idx] = @intCast(desc_idx); - asm volatile ("fence" ::: .{ .memory = true }); + transport_mod.io_barrier(); q.avail.idx +%= 1; - asm volatile ("fence" ::: .{ .memory = true }); + transport_mod.io_barrier(); self.transport.notify(1); uart.print("[VirtIO TX] Queued & Notified Len="); diff --git a/hal/virtio_pci.zig b/hal/virtio_pci.zig index 4bfe171..b8dda63 100644 --- a/hal/virtio_pci.zig +++ b/hal/virtio_pci.zig @@ -14,6 +14,7 @@ //! Dynamically assigns BARs (Base Address Registers) if unassigned by firmware. const std = @import("std"); +const builtin = @import("builtin"); const uart = @import("uart.zig"); // PCI Config Offsets @@ -316,6 +317,17 @@ pub const VirtioTransport = struct { } } + pub fn set_queue_size(self: *VirtioTransport, size: u16) void { + // PCI legacy: queue size is read-only (device sets it) + // Modern: could set via common_cfg.queue_size + if (self.is_modern) { + if (self.common_cfg) |cfg| { + cfg.queue_size = size; + } + } + // Legacy PCI: queue size is fixed by device, no register to write + } + pub fn setup_legacy_queue(self: *VirtioTransport, pfn: u32) void { // Only for legacy @as(*volatile u32, @ptrFromInt(self.legacy_bar + 0x08)).* = pfn; @@ -345,8 +357,65 @@ pub const VirtioTransport = struct { notify_ptr.* = queue_idx; } } + + // ========================================================= + // Unified Accessor API (matches MMIO transport) + // ========================================================= + + pub fn get_device_features(self: *VirtioTransport) u64 { + if (self.is_modern) { + const cfg = self.common_cfg.?; + cfg.device_feature_select = 0; + io_barrier(); + const low: u64 = cfg.device_feature; + cfg.device_feature_select = 1; + io_barrier(); + const high: u64 = cfg.device_feature; + return (high << 32) | low; + } else { + // Legacy: features at offset 0x00 (32-bit only) + return @as(*volatile u32, @ptrFromInt(self.legacy_bar + 0x00)).*; + } + } + + pub fn set_driver_features(self: *VirtioTransport, features: u64) void { + if (self.is_modern) { + const cfg = self.common_cfg.?; + cfg.driver_feature_select = 0; + cfg.driver_feature = @truncate(features); + io_barrier(); + cfg.driver_feature_select = 1; + cfg.driver_feature = @truncate(features >> 32); + io_barrier(); + } else { + // Legacy: guest features at offset 0x04 (32-bit only) + @as(*volatile u32, @ptrFromInt(self.legacy_bar + 0x04)).* = @truncate(features); + } + } + + pub fn get_device_config_byte(self: *VirtioTransport, offset: usize) u8 { + if (self.is_modern) { + if (self.device_cfg) |cfg| { + const ptr: [*]volatile u8 = @ptrCast(cfg); + return ptr[offset]; + } + return 0; + } else { + // Legacy: device config starts at offset 20 + return @as(*volatile u8, @ptrFromInt(self.legacy_bar + 20 + offset)).*; + } + } }; +/// Arch-safe memory barrier for VirtIO I/O ordering. +pub inline fn io_barrier() void { + switch (builtin.cpu.arch) { + .riscv64 => asm volatile ("fence" ::: .{ .memory = true }), + .aarch64 => asm volatile ("dmb sy" ::: .{ .memory = true }), + else => @compileError("unsupported arch"), + } +} + // Modern Config Structure Layout pub const VirtioPciCommonCfg = extern struct { device_feature_select: u32,