rumpk/hal/entry_riscv.zig

572 lines
16 KiB
Zig

// 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: RISC-V Entry Point (Sovereign Trap Architecture)
//!
//! This is the hardware floor for RISC-V64. Sets up stack, trap vectors,
//! S-mode transition, and memory management before handing off to Nim.
//!
//! SAFETY: Runs in bare-metal S-mode with Sv39 paging.
const std = @import("std");
const uart = @import("uart.zig");
// const vm = @import("vm_riscv.zig");
const mm = @import("mm.zig");
const stubs = @import("stubs.zig"); // Force compile stubs
const uart_input = @import("uart_input.zig");
const virtio_net = @import("virtio_net.zig");
comptime {
_ = stubs;
}
// =========================================================
// Entry Point (Naked)
// =========================================================
export fn riscv_init() callconv(.naked) noreturn {
asm volatile (
// 1. Disable Interrupts
\\ csrw sie, zero
\\ csrw satp, zero
\\ csrw sscratch, zero
// PROOF OF LIFE: Raw UART write before ANY initialization
\\ li t0, 0x10000000 // UART base address
\\ li t1, 0x58 // 'X'
\\ sb t1, 0(t0) // Write to THR
// 1.1 Enable FPU (FS), Vectors (VS), and SUM (Supervisor User Memory Access)
\\ li t0, 0x42200 // SUM=bit 18, FS=bit 13, VS=bit 9
\\ csrs sstatus, t0
// 1.2 Initialize Global Pointer
\\ .option push
\\ .option norelax
\\ la gp, __global_pointer$
\\ .option pop
// 1.5 Clear BSS (Zero out uninitialized globals)
\\ la t0, __bss_start
\\ la t1, __bss_end
\\ bge t0, t1, 2f
\\ 1:
\\ sb zero, (t0)
\\ addi t0, t0, 1
\\ blt t0, t1, 1b
\\ 2:
// 2. Set up Stack
\\ la sp, stack_bytes
\\ li t0, 65536
\\ add sp, sp, t0
// 2.1 Install Trap Handler (Direct Mode)
\\ la t0, trap_entry
\\ csrw stvec, t0
// 3. Jump to Zig Entry
\\ call zig_entry
\\ 1: wfi
\\ j 1b
);
// unreachable;
}
// Trap Frame Layout (Packed on stack)
const TrapFrame = extern struct {
ra: usize,
gp: usize,
tp: usize,
t0: usize,
t1: usize,
t2: usize,
s0: usize,
s1: usize,
a0: usize,
a1: usize,
a2: usize,
a3: usize,
a4: usize,
a5: usize,
a6: usize,
a7: usize,
s2: usize,
s3: usize,
s4: usize,
s5: usize,
s6: usize,
s7: usize,
s8: usize,
s9: usize,
s10: usize,
s11: usize,
t3: usize,
t4: usize,
t5: usize,
t6: usize,
sepc: usize,
sstatus: usize,
scause: usize,
stval: usize,
};
// Full Context Save Trap Entry
export fn trap_entry() align(4) callconv(.naked) void {
asm volatile (
// 🔧 CRITICAL FIX: Stack Switching (User -> Kernel)
// Swap sp and sscratch.
// If from User: sp=KStack, sscratch=UStack
// If from Kernel: sp=0, sscratch=ValidStack (Problematic logic if not careful)
// Correct Logic:
// If sscratch == 0: We came from Kernel. sp is already KStack. Do NOTHING to sp.
// If sscratch != 0: We came from User. sp is UStack. Swap to get KStack.
\\ csrrw sp, sscratch, sp
\\ bnez sp, 1f
// Kernel -> Kernel (recursive). Restore sp from sscratch (which had the 0).
\\ csrrw sp, sscratch, sp
\\ 1:
// Allocation (36*8 = 288 bytes)
\\ addi sp, sp, -288
// Save Registers (GPRs)
\\ sd ra, 0(sp)
\\ sd gp, 8(sp)
\\ sd tp, 16(sp)
\\ sd t0, 24(sp)
\\ sd t1, 32(sp)
\\ sd t2, 40(sp)
\\ sd s0, 48(sp)
\\ sd s1, 56(sp)
\\ sd a0, 64(sp)
\\ sd a1, 72(sp)
\\ sd a2, 80(sp)
\\ sd a3, 88(sp)
\\ sd a4, 96(sp)
\\ sd a5, 104(sp)
\\ sd a6, 112(sp)
\\ sd a7, 120(sp)
\\ sd s2, 128(sp)
\\ sd s3, 136(sp)
\\ sd s4, 144(sp)
\\ sd s5, 152(sp)
\\ sd s6, 160(sp)
\\ sd s7, 168(sp)
\\ sd s8, 176(sp)
\\ sd s9, 184(sp)
\\ sd s10, 192(sp)
\\ sd s11, 200(sp)
\\ sd t3, 208(sp)
\\ sd t4, 216(sp)
\\ sd t5, 224(sp)
\\ sd t6, 232(sp)
// RELOAD KERNEL GLOBAL POINTER (Critical for globals access)
\\ .option push
\\ .option norelax
\\ la gp, __global_pointer$
\\ .option pop
// Save CSRs
\\ csrr t0, sepc
\\ sd t0, 240(sp)
\\ csrr t1, sstatus
\\ sd t1, 248(sp)
\\ csrr t2, scause
\\ sd t2, 256(sp)
\\ csrr t3, stval
\\ sd t3, 264(sp)
// Call Handler (Arg0 = Frame Pointer)
\\ mv a0, sp
\\ call rss_trap_handler
// Restore CSRs (Optional if modified? sepc changed for syscall)
\\ ld t0, 240(sp)
\\ csrw sepc, t0
// sstatus often modified to change mode? For return, we use sret.
// We might want to restore sstatus if we support nested interrupts properly.
\\ ld t1, 248(sp)
\\ csrw sstatus, t1
// Restore Encapsulated User Context
\\ ld ra, 0(sp)
\\ ld gp, 8(sp)
\\ ld tp, 16(sp)
\\ ld t0, 24(sp)
\\ ld t1, 32(sp)
\\ ld t2, 40(sp)
\\ ld s0, 48(sp)
\\ ld s1, 56(sp)
\\ ld a0, 64(sp)
\\ ld a1, 72(sp)
\\ ld a2, 80(sp)
\\ ld a3, 88(sp)
\\ ld a4, 96(sp)
\\ ld a5, 104(sp)
\\ ld a6, 112(sp)
\\ ld a7, 120(sp)
\\ ld s2, 128(sp)
\\ ld s3, 136(sp)
\\ ld s4, 144(sp)
\\ ld s5, 152(sp)
\\ ld s6, 160(sp)
\\ ld s7, 168(sp)
\\ ld s8, 176(sp)
\\ ld s9, 184(sp)
\\ ld s10, 192(sp)
\\ ld s11, 200(sp)
\\ ld t3, 208(sp)
\\ ld t4, 216(sp)
\\ ld t5, 224(sp)
\\ ld t6, 232(sp)
// Deallocate stack
\\ addi sp, sp, 288
// 🔧 CRITICAL FIX: Swap back sscratch <-> sp ONLY if returning to User Mode
// Check sstatus.SPP (Bit 8). 0 = User, 1 = Supervisor.
\\ csrr t0, sstatus
\\ li t1, 0x100
\\ and t0, t0, t1
\\ bnez t0, 2f
// Returning to User: Swap sp (Kernel Stack) with sscratch (User Stack)
\\ csrrw sp, sscratch, sp
\\ 2:
\\ sret
);
}
// L1 Kernel Logic
extern fn k_handle_syscall(nr: usize, a0: usize, a1: usize, a2: usize) usize;
extern fn k_handle_exception(scause: usize, sepc: usize, stval: usize) void;
extern fn k_check_deferred_yield() void;
// Memory Management (Page Tables)
extern fn mm_get_kernel_satp() u64;
extern fn mm_activate_satp(satp_val: u64) void;
extern fn k_get_current_satp() u64;
fn get_sstatus() u64 {
return asm volatile ("csrr %[ret], sstatus"
: [ret] "=r" (-> u64),
);
}
fn set_sum() void {
asm volatile ("csrrs zero, sstatus, %[val]"
:
: [val] "r" (@as(u64, 1 << 18)),
);
}
// Global recursion counter
var trap_depth: usize = 0;
export fn rss_trap_handler(frame: *TrapFrame) void {
// 🔥 CRITICAL: Restore kernel page table IMMEDIATELY on trap entry
// const kernel_satp = mm_get_kernel_satp();
// if (kernel_satp != 0) {
// mm_activate_satp(kernel_satp);
// }
// RECURSION GUARD
trap_depth += 1;
if (trap_depth > 3) { // Allow some recursion (e.g. syscall -> fault), but prevent infinite loops
uart.print("[Trap] Infinite Loop Detected. Halting.\n");
while (true) {}
}
defer trap_depth -= 1;
const scause = frame.scause;
// DEBUG: Diagnose Userland Crash (Only print exceptions, ignore interrupts for noise)
if ((scause >> 63) == 0) {
uart.print("\n[Trap] Exception! Cause:");
uart.print_hex(scause);
uart.print(" PC:");
uart.print_hex(frame.sepc);
uart.print(" Val:");
uart.print_hex(frame.stval);
uart.print("\n");
}
// Check high bit: 0 = Exception, 1 = Interrupt
if ((scause >> 63) != 0) {
const intr_id = scause & 0x7FFFFFFFFFFFFFFF;
if (intr_id == 9) {
// PLIC Context 1 (Supervisor) Claim/Complete Register
const PLIC_CLAIM: *volatile u32 = @ptrFromInt(0x0c201004);
const irq = PLIC_CLAIM.*;
if (irq == 10) { // UART0 is IRQ 10 on Virt machine
uart.print("[IRQ] 10\n");
uart_input.poll_input();
} else if (irq >= 32 and irq <= 35) {
virtio_net.virtio_net_poll();
} else if (irq == 0) {
// Spurious or no pending interrupt
} else {
// uart.print("[IRQ] Unknown: ");
// uart.print_hex(irq);
// uart.print("\n");
}
// Complete the IRQ
PLIC_CLAIM.* = irq;
} else if (intr_id == 5) {
// Timer Interrupt
asm volatile ("csrc sip, %[mask]"
:
: [mask] "r" (@as(u64, 1 << 5)),
);
k_check_deferred_yield();
} else {
// uart.print("[Trap] Unhandled Interrupt: ");
// uart.print_hex(intr_id);
// uart.print("\n");
}
} else {
// EXCEPTION HANDLING
// 8: ECALL from U-mode
// 9: ECALL from S-mode
if (scause == 8 or scause == 9) {
const nr = frame.a7;
const a0 = frame.a0;
const a1 = frame.a1;
const a2 = frame.a2;
uart.print("[Syscall] NR:");
uart.print_hex(nr);
uart.print("\n");
// Advance PC to avoid re-executing ECALL
frame.sepc += 4;
// Dispatch Sycall
const ret = k_handle_syscall(nr, a0, a1, a2);
frame.a0 = ret;
} else {
// Delegate all other exceptions to the Kernel Immune System
// This function should NOT return ideally, but if it does, we loop.
k_handle_exception(scause, frame.sepc, frame.stval);
while (true) {}
}
}
// 🔥 CRITICAL RETURN PATH: Restore User Page Table if returning to User Mode
// We check sstatus.SPP (Supervisor Previous Privilege) - Bit 8
// 0 = User, 1 = Supervisor
const sstatus = get_sstatus();
const spp = (sstatus >> 8) & 1;
if (spp == 0) {
const user_satp = k_get_current_satp();
if (user_satp != 0) {
// Enable SUM (Supervisor Access User Memory) so we can read the stack
// to restore registers (since stack is mapped in User PT)
set_sum();
mm_activate_satp(user_satp);
}
}
}
// SAFETY(Stack): Memory is immediately used by _start before any read.
// Initialized to `undefined` for performance (no zeroing 64KB at boot).
export var stack_bytes: [64 * 1024]u8 align(16) = undefined;
const hud = @import("hud.zig");
extern fn kmain() void;
extern fn NimMain() void;
extern fn rumpk_timer_handler() void;
export fn zig_entry() void {
uart.init_riscv();
// 🔧 CRITICAL FIX: Enable SUM (Supervisor User Memory) Access
// S-mode needs to write to U-mode pages (e.g. loading apps at 0x88000000)
// sstatus.SUM is bit 18 (0x40000)
asm volatile (
\\ li t0, 0x40000
\\ csrs sstatus, t0
);
uart.print("[Rumpk L0] zig_entry reached\n");
uart.print("[Rumpk RISC-V] Handing off to Nim L1...\n");
// Networking is initialized by kmain -> rumpk_net_init
NimMain();
kmain();
rumpk_halt();
}
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);
}
const virtio_block = @import("virtio_block.zig");
extern fn hal_surface_init() void;
export fn hal_io_init() void {
uart.init();
hal_surface_init();
// Network init is now called explicitly by kernel (rumpk_net_init)
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) {
asm volatile ("wfi");
}
}
// RISC-V Time Constants
const TIMEBASE: u64 = 10_000_000; // QEMU 'virt' machine (10 MHz)
const SBI_TIME_EID: u64 = 0x54494D45;
fn rdtime() u64 {
var ticks: u64 = 0;
asm volatile ("rdtime %[ticks]"
: [ticks] "=r" (ticks),
);
return ticks;
}
fn sbi_set_timer(stime_value: u64) void {
asm volatile (
\\ ecall
:
: [arg0] "{a0}" (stime_value),
[eid] "{a7}" (SBI_TIME_EID),
[fid] "{a6}" (0), // FID 0 = set_timer
: .{ .memory = true });
}
export fn rumpk_timer_now_ns() u64 {
return rdtime() * 100; // 10MHz = 100ns/tick
}
export fn rumpk_timer_set_ns(interval_ns: u64) void {
if (interval_ns == std.math.maxInt(u64)) {
sbi_set_timer(std.math.maxInt(u64));
// Disable STIE
asm volatile ("csrc sie, %[mask]"
:
: [mask] "r" (@as(usize, 1 << 5)),
);
return;
}
const ticks = interval_ns / 100; // 100ns per tick for 10MHz
const now = rdtime();
const next_time = now + ticks;
sbi_set_timer(next_time);
// Enable STIE (Supervisor Timer Interrupt Enable)
asm volatile ("csrs sie, %[mask]"
:
: [mask] "r" (@as(usize, 1 << 5)),
);
}
// =========================================================
// KEXEC (The Phoenix Protocol)
// =========================================================
export fn hal_kexec(entry: u64, dtb: u64) noreturn {
// 1. Disable Interrupts
asm volatile ("csrc sstatus, 2");
// 2. Disable MMU (Return to Physical Reality)
// WARNING: This assumes we are Identity Mapped (VA=PA) or executing from a location
// where PA is the same. mm.zig creates Identity Map for Kernel code.
asm volatile ("csrw satp, zero");
asm volatile ("sfence.vma zero, zero");
// 3. Jump to new kernel
asm volatile (
\\ jr %[entry]
:
: [entry] "r" (entry),
[dtb] "{a1}" (dtb),
[hart] "{a0}" (0),
);
unreachable;
}
// =========================================================
// USERLAND TRANSITION
// =========================================================
export fn hal_enter_userland(entry: u64, systable: u64, sp: u64) callconv(.c) void {
// 1. Set up sstatus: SPP=0 (User), SPIE=1 (Enable interrupts on return)
// 2. Set sepc to entry point
// 3. Set sscratch to current kernel stack
// 4. Transition via sret
var kstack: usize = 0;
asm volatile ("mv %[kstack], sp"
: [kstack] "=r" (kstack),
);
asm volatile (
\\ li t0, 0x20 // sstatus.SPIE = 1 (bit 5)
\\ csrs sstatus, t0
\\ li t1, 0x100 // sstatus.SPP = 1 (bit 8)
\\ csrc sstatus, t1
\\ li t2, 0x40000 // sstatus.SUM = 1 (bit 18)
\\ csrs sstatus, t2
\\ csrw sepc, %[entry]
\\ csrw sscratch, %[kstack]
\\ mv sp, %[sp]
\\ mv a0, %[systable]
\\ sret
:
: [entry] "r" (entry),
[systable] "r" (systable),
[sp] "r" (sp),
[kstack] "r" (kstack),
);
}