code wiki / _hdl_build / rv64im_min_sim.nx

rv64im_min_sim.nx source

↩ module page · 1246 lines · 59654 B

1// rv64im_min_sim.nx -- single-cycle RV64IM-min behavioural simulator. 2// 3// First runnable thing in nishi-silicon. Ties the previously-shipped 4// modules together so RV64IM programs execute end-to-end in pure 5// NishiLang: 6// 7// decoder (rv64im_min_decoder) -> opcode kind + operand fields 8// ALU (rv64im_min_alu) -> arithmetic compute 9// regfile (rv64im_min_regfile) -> 32 GPRs storage 10// CSR (rv64im_min_csr) -> M-mode CSRs 11// CLINT (rv64im_min_clint) -> MTIP interrupt source 12// UART (rv64im_min_uart) -> debug console 13// 14// Why "single-cycle" not "pipelined": a single-cycle model is the 15// canonical "first simulator" -- one instruction per simulator step, 16// no pipeline hazards, no fetch/decode/execute overlap. Verifies 17// the combinational + sequential modules without the complexity of 18// IF/ID/EX latches or hazard detection. Pipelined model lands in a 19// follow-on commit; Tier A FPGA bitstream gets the pipelined version 20// (no point fabbing a single-cycle core when the pipelined one is 21// also single-issue in-order and only marginally larger). 22// 23// What this enables: 24// - Run RV64IM .text segments emitted by the SOVEREIGN toolchain (nx_cc 25// -> nxasm rv64 backend); gcc/llvm are a diff-lane reference ONLY, never on 26// the chain (this sim IS the genesis ISA spec + the sovereign-cycle yardstick) 27// - Validate the decoder + ALU against any RV64IM disassembler 28// - Boot the nishi-os kernel ELF (after the load/store unit + memory 29// bus land in a follow-on commit) before fabbing FPGA bitstream 30// 31// Status: SEED. 2026-05-26. Single-cycle non-interrupt loop. 32// Interrupt handling, load/store, mret/ecall flow land per the TODO 33// markers below. 34 35import "nx_syscalls.nx" 36import "nishi_hdl_primitives.nx" 37import "rv64im_min_decoder.nx" 38import "rv64im_min_alu.nx" 39import "rv64im_min_regfile.nx" 40import "rv64im_min_csr.nx" 41import "rv64im_min_clint.nx" 42import "rv64im_min_uart.nx" 43import "rv64im_min_virtio.nx" 44import "rv64im_min_nvme.nx" 45import "rv64im_min_nndev.nx" 46import "rv64im_min_mmu.nx" 47import "rv64im_min_gpu.nx" 48import "nx_rvc_expand.nx" // RV64 C (compressed) extension: expand 16-bit -> 32-bit at fetch 49 50// ===== Sim state ================================================= 51// 52// The simulator owns instances of every device and a PC register. 53// Memory is a flat byte buffer with the CLINT + UART carve-outs 54// dispatched by the load/store path. 55 56struct NxRv64imSim { 57 pc: i64 58 rf: *NxRv64imRegfile 59 csr: *NxRv64imCsrFile 60 clint: *NxClint 61 uart: *NxUart 62 virtio: *NxVirtioMmio // legacy virtio-MMIO transport, blk @0x10001000 (K-R2-001a) 63 virtio_net:*NxVirtioMmio // second legacy virtio-MMIO transport, net @0x10002000 (K-R2-001c1) 64 nvme: *NxNvmeCtrl // NVMe-class controller, admin SQ/CQ @0x10003000 (X-DRV-W1 stage B) 65 nndev: *NxNnDev // Nishi-native device protocol (NNDP) @0x10005000 (sovereign-device-protocol exceed) 66 mmu: *NxMmu // Sv39 page-table-walk device @0x10006000 (virtual-memory-paging-mmu) 67 xlate_fault: i64 // set by nx_rv64im_xlate on a data-access page fault; step loop traps on it 68 gpu: *NxGpu // GPU-class command-submission controller @0x10007000 (driver-from-spec D) 69 mem_base: i64 // physical address of mem_buf[0] (e.g., 0x80000000) 70 mem_buf: *u8 // byte-addressable RAM 71 mem_size: i64 // bytes 72 valid: i64 73 halted: i64 74 halt_code: i64 // exit code on halt 75 steps: i64 // instructions executed 76 priv: i64 // current privilege: 3=M, 1=S, 0=U (reset M). Sv39 translation 77 // applies only in S/U; M-mode is always Bare (riscv-priv-spec). 78 xlate_store: i64 // 1 if the in-flight data access is a store (page-fault cause select) 79 // ----- Per-PC cycle attribution (M2 silicon-feedback foundation) ----- 80 // pc_cycles[i] = total cycles attributable to PC = mem_base + (i << 2). 81 // Indexed by word (4-byte stride) since RV64IM instructions are 32-bit. 82 // Caller allocates pc_cycles_buf with at least (mem_size >> 2) i64s. 83 // Without these the substrate has no per-instruction perf data; with 84 // them the silicon-feedback loop (M3) can pick out top-N hot PCs. 85 pc_cycles_buf: *i64 86 pc_cycles_cap: i64 // capacity in i64 slots 87 pc_cycles_on: i64 // 1 = attribution active; 0 = skip (cheaper hot loop) 88 // ----- A-extension (atomics) LR/SC reservation (F107h, 2026-07-18) ----- 89 // Single-address reservation set: lr.w/d records (resv_addr, resv_valid); sc.w/d succeeds 90 // iff still valid AND matches; any sc or amo* clears it. Single-hart model -> an lr/sc pair 91 // with no intervening reservation-breaker succeeds (matches QEMU on the canonical CAS loop); 92 // multi-hart contention is the F107g follow-on. 93 resv_addr: i64 94 resv_valid: i64 95} 96 97// ===== Sim init ================================================= 98// 99// Caller provides allocated regfile/csr/clint/uart + memory buffer. 100// Sim init initialises PC to mem_base + entry_offset (typically 0 101// for kernel ELFs which start at the base of mem). 102 103func nx_rv64im_sim_init(s: *NxRv64imSim, 104 rf: *NxRv64imRegfile, 105 csr: *NxRv64imCsrFile, 106 clint: *NxClint, 107 uart: *NxUart, 108 mem_base: i64, 109 mem_buf: *u8, 110 mem_size: i64, 111 entry_offset: i64) -> i64 { 112 if (s as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 113 if (mem_buf as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 114 if mem_size <= 0 { return 0 - NX_HDL_BAD_KIND } 115 s.pc = mem_base + entry_offset 116 s.rf = rf 117 s.csr = csr 118 s.clint = clint 119 s.uart = uart 120 // virtio attaches separately via nx_rv64im_sim_attach_virtio (keeps the init 121 // signature stable -- API contract stability). Null until the runner wires it. 122 s.virtio = (0 as i64) as *NxVirtioMmio 123 s.virtio_net = (0 as i64) as *NxVirtioMmio 124 s.nvme = (0 as i64) as *NxNvmeCtrl 125 s.nndev = (0 as i64) as *NxNnDev 126 s.mmu = (0 as i64) as *NxMmu 127 s.gpu = (0 as i64) as *NxGpu 128 s.xlate_fault = 0 129 s.priv = 3 // reset to M-mode 130 s.xlate_store = 0 131 s.mem_base = mem_base 132 s.mem_buf = mem_buf 133 s.mem_size = mem_size 134 s.valid = 1 135 s.halted = 0 136 s.halt_code = 0 137 s.steps = 0 138 // Cycle attribution opt-in: caller wires nx_rv64im_sim_enable_pc_cycles 139 // after init. Default off so the hot step loop stays branch-free for 140 // bench runs that don't need profiling. 141 s.pc_cycles_buf = (0 as i64) as *i64 142 s.pc_cycles_cap = 0 143 s.pc_cycles_on = 0 144 // A-extension reservation starts clear (no lr outstanding). 145 s.resv_addr = 0 146 s.resv_valid = 0 147 return NX_HDL_OK 148} 149 150// ===== virtio-MMIO attach (K-R2-001a) ================================================= 151// 152// Wires a (already-init'd) virtio-MMIO transport into the sim so load32/store32 route 153// the 0x10001000..NX_VIRTIO_END window to it. Kept separate from sim_init so the init 154// signature stays stable. Null virtio = no device (load32/store32 fall through to 0). 155func nx_rv64im_sim_attach_virtio(s: *NxRv64imSim, virtio: *NxVirtioMmio) -> i64 { 156 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND } 157 s.virtio = virtio 158 return NX_HDL_OK 159} 160 161// ===== second virtio-MMIO attach: virtio-net (K-R2-001c1) ================================= 162// Wires the net device (DeviceID=1) so load32/store32 route the 0x10002000..NX_VIRTIO_NET_END 163// window to it. Independent of the blk device -- the blk window (0x10001000) is untouched. 164func nx_rv64im_sim_attach_virtio_net(s: *NxRv64imSim, virtio_net: *NxVirtioMmio) -> i64 { 165 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND } 166 s.virtio_net = virtio_net 167 return NX_HDL_OK 168} 169 170// ===== NVMe-class controller attach (X-DRV-W1 stage B) ================================= 171// Wires the NVMe controller (admin SQ/CQ @0x10003000) so load32/store32 route the 172// 0x10003000..NX_NVME_END window to it. Independent of the virtio devices -- the blk + net 173// windows are untouched (additive). Null nvme = no device (the window falls through to 0). 174func nx_rv64im_sim_attach_nvme(s: *NxRv64imSim, nvme: *NxNvmeCtrl) -> i64 { 175 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND } 176 s.nvme = nvme 177 return NX_HDL_OK 178} 179 180// ===== Nishi-native device protocol attach (sovereign-device-protocol exceed) ================= 181// Wires the NNDP device (NND1 @0x10004000) so load32/store32 route the 0x10004000..NX_NNDEV_END 182// window to it. Independent of virtio + nvme (their windows are untouched -- additive). Null 183// nndev = no device (the window falls through to 0). 184func nx_rv64im_sim_attach_nndev(s: *NxRv64imSim, nndev: *NxNnDev) -> i64 { 185 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND } 186 s.nndev = nndev 187 return NX_HDL_OK 188} 189 190// ===== GPU-class controller attach (driver-from-spec instance D, GPU trajectory) ================= 191// Wires the GPU controller (command ring @0x10007000) so load32/store32 route the 0x10007000.. 192// NX_GPU_END window to it. Independent of the other devices -- their windows are untouched (additive). 193func nx_rv64im_sim_attach_gpu(s: *NxRv64imSim, gpu: *NxGpu) -> i64 { 194 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND } 195 s.gpu = gpu 196 return NX_HDL_OK 197} 198 199// ===== Sv39 MMU walk device attach (virtual-memory-paging-mmu) ================================= 200// Wires the Sv39 page-table-walk device (@0x10006000) so load32/store32 route the 0x10006000.. 201// NX_MMU_END window to it. Independent of all other devices (additive). Null mmu = no device. 202func nx_rv64im_sim_attach_mmu(s: *NxRv64imSim, mmu: *NxMmu) -> i64 { 203 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND } 204 s.mmu = mmu 205 return NX_HDL_OK 206} 207 208// ===== Cycle attribution opt-in ================================================= 209// 210// Hand in a pre-allocated buffer of (mem_size >> 2) i64s (one slot per 211// 4-byte instruction word). Once enabled, every step() bumps the slot 212// for the executing PC. Disabled paths skip the bump in 1 branch. 213 214func nx_rv64im_sim_enable_pc_cycles(s: *NxRv64imSim, 215 buf: *i64, cap: i64) -> i64 { 216 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND } 217 if (buf as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 218 let need: i64 = s.mem_size >> 2 219 if cap < need { return 0 - NX_HDL_GRAPH_FULL } 220 s.pc_cycles_buf = buf 221 s.pc_cycles_cap = cap 222 s.pc_cycles_on = 1 223 var i: i64 = 0 224 while i < cap { buf[i] = 0; i = i + 1 } 225 return NX_HDL_OK 226} 227 228// Read cycles attributable to a specific PC (returns 0 if out of range 229// or attribution disabled). 230func nx_rv64im_sim_pc_cycles_at(s: *NxRv64imSim, pc: i64) -> i64 { 231 if s.pc_cycles_on != 1 { return 0 } 232 if pc < s.mem_base { return 0 } 233 let off: i64 = (pc - s.mem_base) >> 2 234 if off < 0 { return 0 } 235 if off >= s.pc_cycles_cap { return 0 } 236 return s.pc_cycles_buf[off] 237} 238 239// Pick the top-N hottest PCs. out_pcs[i] / out_cycles[i] filled with 240// the i-th hottest (descending). Returns count actually filled (<= n). 241// V1 is O(cap * n) -- fine for n <= ~50; better algorithm when N gets 242// large. 243func nx_rv64im_sim_pc_cycles_topn(s: *NxRv64imSim, n: i64, 244 out_pcs: *i64, out_cycles: *i64) -> i64 { 245 if s.pc_cycles_on != 1 { return 0 } 246 if (out_pcs as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 247 if (out_cycles as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 248 var filled: i64 = 0 249 while filled < n { 250 var best_off: i64 = 0 - 1 251 var best_val: i64 = 0 252 var i: i64 = 0 253 while i < s.pc_cycles_cap { 254 let v: i64 = s.pc_cycles_buf[i] 255 // Skip slots already picked (encoded by negating after pick). 256 if v > best_val { 257 best_val = v 258 best_off = i 259 } 260 i = i + 1 261 } 262 if best_off < 0 { return filled } 263 if best_val == 0 { return filled } 264 out_pcs[filled] = s.mem_base + (best_off << 2) 265 out_cycles[filled] = best_val 266 // Mark picked by zeroing the slot. Destructive but acceptable 267 // for a one-shot top-N read; caller can re-enable + re-run if 268 // they need fresh data. 269 s.pc_cycles_buf[best_off] = 0 270 filled = filled + 1 271 } 272 return filled 273} 274 275// ===== Memory access (load/store) ================================================= 276// 277// Routes physical addresses to the right device: 278// - RAM (mem_base..mem_base+mem_size) -> mem_buf 279// - CLINT (0x02000000..0x0200C000) -> clint MMIO 280// - UART (0x10000000..0x10000008) -> uart MMIO 281// - Anything else -> halt (illegal access) 282 283func nx_rv64im_sim_load8(s: *NxRv64imSim, addr: i64) -> i64 { 284 if addr >= s.mem_base { 285 let off: i64 = addr - s.mem_base 286 if off < s.mem_size { 287 return s.mem_buf[off] as i64 288 } 289 } 290 // UART path: LSR polling reads via lbu. Returns the byte in 291 // the low 8 bits; sign-extension done by the caller per funct3. 292 if addr >= NX_UART_BASE { if addr < NX_UART_END { 293 let out: *i64 = (sys_mmap(8)) as *i64 294 out[0] = 0 295 nx_uart_read8(s.uart, addr, out) 296 return out[0] & 0xff 297 }} 298 // CLINT byte reads not used by kernel; return 0. 299 return 0 300} 301 302func nx_rv64im_sim_load16(s: *NxRv64imSim, addr: i64) -> i64 { 303 if addr >= s.mem_base { 304 let off: i64 = addr - s.mem_base 305 if off < s.mem_size - 1 { 306 let b0: i64 = s.mem_buf[off] as i64 307 let b1: i64 = s.mem_buf[off + 1] as i64 308 return b0 | (b1 << 8) 309 } 310 } 311 return 0 312} 313 314func nx_rv64im_sim_load32(s: *NxRv64imSim, addr: i64) -> i64 { 315 if addr >= s.mem_base { 316 let off: i64 = addr - s.mem_base 317 if off < s.mem_size - 3 { 318 let b0: i64 = s.mem_buf[off] as i64 319 let b1: i64 = s.mem_buf[off + 1] as i64 320 let b2: i64 = s.mem_buf[off + 2] as i64 321 let b3: i64 = s.mem_buf[off + 3] as i64 322 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) 323 } 324 } 325 // virtio-MMIO transport path (K-R2-001a): lw from the device register window 326 // returns the device-modelled 32-bit register value (identity / features / status). 327 if addr >= NX_VIRTIO_BASE { if addr < NX_VIRTIO_END { 328 if (s.virtio as i64) != 0 { 329 let vout: *i64 = (sys_mmap(8)) as *i64 330 vout[0] = 0 331 nx_virtio_read32(s.virtio, addr, vout) 332 return vout[0] & 0xffffffff 333 } 334 }} 335 // virtio-net device register window (K-R2-001c1): lw from the second transport block 336 // at 0x10002000 returns DeviceID=1 + the same identity/feature/status surface as blk. 337 if addr >= NX_VIRTIO_NET_BASE { if addr < NX_VIRTIO_NET_END { 338 if (s.virtio_net as i64) != 0 { 339 let nout: *i64 = (sys_mmap(8)) as *i64 340 nout[0] = 0 341 nx_virtio_read32(s.virtio_net, addr, nout) 342 return nout[0] & 0xffffffff 343 } 344 }} 345 // NVMe controller register window (X-DRV-W1 stage B): lw from 0x10003000.. returns CAP/VS, 346 // the latched CC/AQA/ASQ/ACQ regs, CSTS (RDY flop the driver polls), and the CQPEEK binding- 347 // proof instrument. An INDEPENDENT device instance -- the virtio windows are untouched. 348 if addr >= NX_NVME_BASE { if addr < NX_NVME_END { 349 if (s.nvme as i64) != 0 { 350 let mout: *i64 = (sys_mmap(8)) as *i64 351 mout[0] = 0 352 nx_nvme_read32(s.nvme, addr, mout) 353 return mout[0] & 0xffffffff 354 } 355 }} 356 // Nishi-native device window (sovereign-device-protocol): lw from 0x10004000.. returns MAGIC/ 357 // DEVCLASS identity, the latched ENABLE/CMD_ADDR, and the STATUS/RESULT completion the driver 358 // reads back after a doorbell. An INDEPENDENT instance -- virtio + nvme windows untouched. 359 if addr >= NX_NNDEV_BASE { if addr < NX_NNDEV_END { 360 if (s.nndev as i64) != 0 { 361 let dout: *i64 = (sys_mmap(8)) as *i64 362 dout[0] = 0 363 nx_nndev_read32(s.nndev, addr, dout) 364 return dout[0] & 0xffffffff 365 } 366 }} 367 // Sv39 MMU walk device window (virtual-memory-paging-mmu): lw from 0x10006000.. returns the 368 // translated PADDR + FAULT the device latched on the last doorbell walk. Independent instance. 369 if addr >= NX_MMU_BASE { if addr < NX_MMU_END { 370 if (s.mmu as i64) != 0 { 371 let uout: *i64 = (sys_mmap(8)) as *i64 372 uout[0] = 0 373 nx_mmu_read32(s.mmu, addr, uout) 374 return uout[0] & 0xffffffff 375 } 376 }} 377 // GPU-class controller register window (driver-from-spec D): lw from 0x10007000.. returns ID, 378 // CTRL/STATUS, RING base, the FENCE seqno + the RESULTPEEK binding-proof word. Independent instance. 379 if addr >= NX_GPU_BASE { if addr < NX_GPU_END { 380 if (s.gpu as i64) != 0 { 381 let gout: *i64 = (sys_mmap(8)) as *i64 382 gout[0] = 0 383 nx_gpu_read32(s.gpu, addr, gout) 384 return gout[0] & 0xffffffff 385 } 386 }} 387 return 0 388} 389 390// Sign-extend an 8/16/32-bit value to 64 bits. 391func nx_rv64im_sext8(v: i64) -> i64 { 392 let low: i64 = v & 0xff 393 if (low & 0x80) != 0 { return low | (0 - 256) } 394 return low 395} 396func nx_rv64im_sext16(v: i64) -> i64 { 397 let low: i64 = v & 0xffff 398 if (low & 0x8000) != 0 { return low | (0 - 65536) } 399 return low 400} 401 402func nx_rv64im_sim_load64(s: *NxRv64imSim, addr: i64) -> i64 { 403 if addr >= s.mem_base { 404 let off: i64 = addr - s.mem_base 405 if off < s.mem_size - 7 { 406 var v: i64 = 0 407 var i: i64 = 0 408 while i < 8 { 409 let b: i64 = s.mem_buf[off + i] as i64 410 v = v | (b << (i * 8)) 411 i = i + 1 412 } 413 return v 414 } 415 } 416 // CLINT MMIO path 417 if addr >= NX_CLINT_BASE { if addr < NX_CLINT_END { 418 let out: *i64 = (sys_mmap(8)) as *i64 419 out[0] = 0 420 nx_clint_read64(s.clint, addr, out) 421 return out[0] 422 }} 423 return 0 424} 425 426func nx_rv64im_sim_store64(s: *NxRv64imSim, addr: i64, value: i64) -> i64 { 427 if addr >= s.mem_base { 428 let off: i64 = addr - s.mem_base 429 if off < s.mem_size - 7 { 430 var i: i64 = 0 431 while i < 8 { 432 s.mem_buf[off + i] = ((value >> (i * 8)) & 0xff) as u8 433 i = i + 1 434 } 435 return NX_HDL_OK 436 } 437 } 438 if addr >= NX_CLINT_BASE { if addr < NX_CLINT_END { 439 return nx_clint_write64(s.clint, addr, value) 440 }} 441 // UART path (8-bit writes only at THR; 64-bit writes get low byte) 442 if addr >= NX_UART_BASE { if addr < NX_UART_END { 443 return nx_uart_write8(s.uart, addr, value & 0xff) 444 }} 445 return 0 - NX_HDL_BAD_KIND 446} 447 448func nx_rv64im_sim_store8(s: *NxRv64imSim, addr: i64, value: i64) -> i64 { 449 if addr >= s.mem_base { 450 let off: i64 = addr - s.mem_base 451 if off < s.mem_size { 452 s.mem_buf[off] = (value & 0xff) as u8 453 return NX_HDL_OK 454 } 455 } 456 if addr >= NX_UART_BASE { if addr < NX_UART_END { 457 return nx_uart_write8(s.uart, addr, value & 0xff) 458 }} 459 return 0 - NX_HDL_BAD_KIND 460} 461 462func nx_rv64im_sim_store16(s: *NxRv64imSim, addr: i64, value: i64) -> i64 { 463 if addr >= s.mem_base { 464 let off: i64 = addr - s.mem_base 465 if off < s.mem_size - 1 { 466 s.mem_buf[off] = (value & 0xff) as u8 467 s.mem_buf[off + 1] = ((value >> 8) & 0xff) as u8 468 return NX_HDL_OK 469 } 470 } 471 return 0 - NX_HDL_BAD_KIND 472} 473 474func nx_rv64im_sim_store32(s: *NxRv64imSim, addr: i64, value: i64) -> i64 { 475 if addr >= s.mem_base { 476 let off: i64 = addr - s.mem_base 477 if off < s.mem_size - 3 { 478 s.mem_buf[off] = (value & 0xff) as u8 479 s.mem_buf[off + 1] = ((value >> 8) & 0xff) as u8 480 s.mem_buf[off + 2] = ((value >> 16) & 0xff) as u8 481 s.mem_buf[off + 3] = ((value >> 24) & 0xff) as u8 482 return NX_HDL_OK 483 } 484 } 485 // virtio-MMIO transport path (K-R2-001a): sw into the device register window 486 // latches GuestFeatures / drives the Status handshake (ACK/DRIVER/FEATURES_OK/ 487 // DRIVER_OK) or resets the device (Status write of 0). On a QueueNotify kick 488 // (K-R2-001b2a) the device DMA-reads descriptor 0's first field from the QueuePFN- 489 // bound ring page in guest RAM (the sim owns mem_buf, so we hand it in) and latches 490 // it into QueueDescPeek -- proving the device walked the descriptor the driver laid. 491 if addr >= NX_VIRTIO_BASE { if addr < NX_VIRTIO_END { 492 if (s.virtio as i64) != 0 { 493 let wr: i64 = nx_virtio_write32(s.virtio, addr, value) 494 if addr == (NX_VIRTIO_BASE + NX_VIRTIO_OFF_QUEUENOTIFY) { 495 nx_virtio_notify_dma(s.virtio, s.mem_buf, s.mem_base, s.mem_size) 496 } 497 return wr 498 } 499 }} 500 // virtio-net device register window (K-R2-001c1/c2/c3): sw into the second transport block 501 // at 0x10002000 latches GuestFeatures / drives the Status handshake (c1) + the legacy 502 // virtqueue-config registers (c2). On a QueueNotify kick (K-R2-001c3) the net device DMA- 503 // walks the driver's tx ring EXACTLY like the blk branch above: nx_virtio_notify_dma is 504 // device-instance-generic (it walks v.storage[QPFN] -> desc-read + avail/used + the data 505 // round-trip), so handing it s.virtio_net makes the net FRAME word the driver laid in the 506 // tx descriptor's data buffer round-trip into QueueSectPeek with NO new DMA code. The blk 507 // device at 0x10001000 is untouched -- this is the net block's own QueueNotify trigger. 508 if addr >= NX_VIRTIO_NET_BASE { if addr < NX_VIRTIO_NET_END { 509 if (s.virtio_net as i64) != 0 { 510 let wrn: i64 = nx_virtio_write32(s.virtio_net, addr, value) 511 if addr == (NX_VIRTIO_NET_BASE + NX_VIRTIO_OFF_QUEUENOTIFY) { 512 nx_virtio_notify_dma(s.virtio_net, s.mem_buf, s.mem_base, s.mem_size) 513 } 514 return wrn 515 } 516 }} 517 // NVMe controller register window (X-DRV-W1 stage B): sw into 0x10003000.. latches CC (and 518 // sets/clears the CSTS.RDY flop on CC.EN), AQA, the ASQ/ACQ base-address registers, and the 519 // doorbells. On the SQ-Tail doorbell (off_sq0tdbl) the device runs the doorbell DMA: it 520 // fetches the 64-byte SQE from ASQ_base (reads opcode+CID), posts a phase-tagged 16-byte CQE 521 // into ACQ_base, and latches NvmeCqPeek = (CID<<16)|status|phase. The sim owns mem_buf, so 522 // it hands guest RAM in -- mirroring the virtio QueueNotify branch. The virtio devices are 523 // untouched -- this is the NVMe instance's own doorbell trigger. 524 if addr >= NX_NVME_BASE { if addr < NX_NVME_END { 525 if (s.nvme as i64) != 0 { 526 let wrm: i64 = nx_nvme_write32(s.nvme, addr, value) 527 if addr == (NX_NVME_BASE + NX_NVME_OFF_SQ0TDBL) { 528 nx_nvme_doorbell_dma(s.nvme, s.mem_buf, s.mem_base, s.mem_size) 529 } 530 return wrm 531 } 532 }} 533 // Nishi-native device window (sovereign-device-protocol): sw into 0x10004000.. latches ENABLE 534 // (single-step bring-up) / CMD_ADDR. On the DOORBELL write the device runs the inline command 535 // at CMD_ADDR (DMA-reads the descriptor + the data buffer in guest RAM, the sim owns mem_buf so 536 // we hand it in) and latches STATUS + RESULT inline -- ONE descriptor, ONE round-trip, NO rings. 537 // virtio + nvme devices are untouched -- this is the NNDP instance's own doorbell trigger. 538 if addr >= NX_NNDEV_BASE { if addr < NX_NNDEV_END { 539 if (s.nndev as i64) != 0 { 540 let wrd: i64 = nx_nndev_write32(s.nndev, addr, value) 541 if addr == (NX_NNDEV_BASE + NX_NNDEV_OFF_DOORBELL) { 542 nx_nndev_doorbell_dma(s.nndev, s.mem_buf, s.mem_base, s.mem_size) 543 } 544 return wrd 545 } 546 }} 547 // Sv39 MMU walk device window (virtual-memory-paging-mmu): sw into 0x10006000.. latches satp / 548 // vaddr. On the DOORBELL write the device WALKS the 3-level Sv39 page table in guest RAM (the 549 // sim owns mem_buf, so we hand it in) and latches PADDR + FAULT. Other devices untouched. 550 if addr >= NX_MMU_BASE { if addr < NX_MMU_END { 551 if (s.mmu as i64) != 0 { 552 let wru: i64 = nx_mmu_write32(s.mmu, addr, value) 553 if addr == (NX_MMU_BASE + NX_MMU_OFF_DOORBELL) { 554 nx_mmu_doorbell_walk(s.mmu, s.mem_buf, s.mem_base, s.mem_size) 555 } 556 return wru 557 } 558 }} 559 // GPU-class controller window (driver-from-spec D): sw into 0x10007000.. latches CTRL (sets/clears 560 // STATUS.READY on CTRL.EN) + the RING base. On the DOORBELL write the device runs the dispatch DMA: 561 // it fetches the command packet from RING base, validates the SPIR-V magic, signals the FENCE + latches 562 // RESULTPEEK. The sim owns mem_buf, so it hands guest RAM in. Other devices untouched. 563 if addr >= NX_GPU_BASE { if addr < NX_GPU_END { 564 if (s.gpu as i64) != 0 { 565 let wrg: i64 = nx_gpu_write32(s.gpu, addr, value) 566 if addr == (NX_GPU_BASE + NX_GPU_OFF_DOORBELL) { 567 nx_gpu_doorbell_dma(s.gpu, s.mem_buf, s.mem_base, s.mem_size) 568 } 569 return wrg 570 } 571 }} 572 // SiFive test/finisher at 0x100000: 32-bit write of 0x5555 = clean 573 // poweroff per kernel tasks.nx:203-204. 574 if addr == 0x100000 { 575 if (value & 0xffff) == 0x5555 { 576 s.halted = 1 577 s.halt_code = 0 // clean exit 578 return NX_HDL_OK 579 } 580 } 581 return 0 - NX_HDL_BAD_KIND 582} 583 584// ===== CPU-datapath Sv39 translation (virtual-memory-paging-mmu, integration rung) ============ 585// 586// When satp.MODE == Sv39 (8), DATA loads/stores translate their address through the page table in 587// guest RAM (the SAME nx_sv39_walk the MMU walk device uses). satp.MODE == 0 (Bare, the reset + 588// every existing test) -> identity, so there is ZERO change to all prior gates. Device MMIO 589// windows stay physical (identity) even under Sv39 in this slice so a paging test can still drive 590// the UART/finisher. Instruction FETCH is NOT translated here (the image is physically addressed); 591// fetch-translation + the page-fault TRAP on a bad data walk are the documented follow-on rungs -- 592// on a walk fault this returns the vaddr unchanged (a tampered mapping then reads out-of-range -> 0, 593// which the gate's verify catches). 594func nx_rv64im_is_device(addr: i64) -> i64 { 595 if addr >= 0x100000 { if addr < 0x100008 { return 1 } } // SiFive finisher 596 if addr >= 0x02000000 { if addr < 0x0200C000 { return 1 } } // CLINT 597 if addr >= 0x10000000 { if addr < 0x10000008 { return 1 } } // UART 598 if addr >= 0x10001000 { if addr < 0x10007000 { return 1 } } // virtio/nvme/nndev/mmu windows 599 return 0 600} 601const NX_ACC_FETCH: i64 = 0 602const NX_ACC_LOAD: i64 = 1 603const NX_ACC_STORE: i64 = 2 604// Translate `vaddr` for an access of type `access` (FETCH/LOAD/STORE) and ENFORCE the page's permission 605// bits (riscv-priv-spec): a fetch needs X (the NX-bit / W^X foundation -- a data page is not executable), 606// a load needs R, a store needs W. A valid leaf lacking the required bit signals xlate_fault, which the 607// step loop routes to the SAME page-fault cause it already uses per access (instr=12/load=13/store=15). 608// Identity in M-mode and under Bare, and device MMIO stays physical -> every physically-addressed path 609// is unchanged (perm enforcement applies only to S/U-mode Sv39 RAM accesses). 610func nx_rv64im_xlate(s: *NxRv64imSim, vaddr: i64, access: i64) -> i64 { 611 if s.priv == 3 { return vaddr } // M-mode: always Bare (riscv-priv-spec) 612 let satp: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_SATP) 613 if ((satp >> 60) & 0xf) != 8 { return vaddr } // Bare / non-Sv39: identity 614 if nx_rv64im_is_device(vaddr) == 1 { return vaddr } // device MMIO stays physical 615 let fb: *i64 = sys_mmap(16) as *i64 616 let pb: *i64 = sys_mmap(16) as *i64 // leaf PTE perm bits (V|R|W|X) from the walk 617 let pab: *i64 = sys_mmap(16) as *i64 // leaf PTE physical address (A/D write-back target) 618 let pa: i64 = nx_sv39_walk(satp, s.mem_buf, s.mem_base, s.mem_size, vaddr, fb, pb, pab) 619 if fb[0] == 1 { s.xlate_fault = 1; return vaddr } // walk fault: signal the step loop to trap 620 var need: i64 = 8 // FETCH needs X (bit 3) 621 if access == NX_ACC_LOAD { need = 2 } // LOAD needs R (bit 1) 622 if access == NX_ACC_STORE { need = 4 } // STORE needs W (bit 2) 623 if (pb[0] & need) == 0 { s.xlate_fault = 1; return vaddr } // R/W/X permission violation -> page fault 624 // USER/SUPERVISOR isolation (riscv-priv-spec, the U-bit = bit 4): a U-mode access REQUIRES a 625 // user page; an S-mode access to a USER page is denied unless mstatus.SUM (Supervisor User Memory). 626 let is_user_page: i64 = pb[0] & 0x10 627 if s.priv == 0 { if is_user_page == 0 { s.xlate_fault = 1; return vaddr } } // U-mode: page must be U=1 628 if s.priv == 1 { if is_user_page != 0 { // S-mode touching a U=1 page 629 let mstatus: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS) 630 if (mstatus & 0x40000) == 0 { s.xlate_fault = 1; return vaddr } // SUM=0 -> deny (kernel can't deref user memory by default) 631 } } 632 // ACCESSED/DIRTY bits (hardware-managed, riscv-priv-spec Svadu): a SUCCESSFUL translation sets A on 633 // any access + D on a store, in the leaf PTE. Additive -- A/D (bits 6/7) don't affect the PA/perm 634 // decode, so every prior test is unchanged; they let the OS drive page replacement + dirty write-back. 635 let pte_off: i64 = pab[0] - s.mem_base 636 if nx_mmu_inrange(pte_off, 8, s.mem_size) == 1 { 637 let cur: i64 = nx_mmu_rd64(s.mem_buf, pte_off) 638 var npte: i64 = cur | 0x40 // A (bit 6) 639 if access == NX_ACC_STORE { npte = npte | 0x80 } // D (bit 7) on a store 640 if npte != cur { nx_mmu_wr64(s.mem_buf, pte_off, npte) } 641 } 642 return pa 643} 644 645// ===== Map decoded {opcode, funct3, funct7} -> ALU op ================================================= 646// 647// The decoder gave us NX_RV64IM_OP_OP / OP_IMM / OP_32 / etc.; we 648// need the finer-grained ALU op for the compute call. 649 650func nx_rv64im_sim_alu_select(op_kind: i64, funct3: i64, funct7: i64) -> i64 { 651 if op_kind == NX_RV64IM_OP_OP { 652 if funct7 == 0x01 { 653 // M extension 654 if funct3 == 0 { return NX_RV64IM_ALU_MUL } 655 if funct3 == 1 { return NX_RV64IM_ALU_MULH } 656 if funct3 == 2 { return NX_RV64IM_ALU_MULHSU } 657 if funct3 == 3 { return NX_RV64IM_ALU_MULHU } 658 if funct3 == 4 { return NX_RV64IM_ALU_DIV } 659 if funct3 == 5 { return NX_RV64IM_ALU_DIVU } 660 if funct3 == 6 { return NX_RV64IM_ALU_REM } 661 if funct3 == 7 { return NX_RV64IM_ALU_REMU } 662 } 663 if funct3 == 0 { 664 if funct7 == 0x20 { return NX_RV64IM_ALU_SUB } 665 return NX_RV64IM_ALU_ADD 666 } 667 if funct3 == 1 { return NX_RV64IM_ALU_SLL } 668 if funct3 == 2 { return NX_RV64IM_ALU_SLT } 669 if funct3 == 3 { return NX_RV64IM_ALU_SLTU } 670 if funct3 == 4 { return NX_RV64IM_ALU_XOR } 671 if funct3 == 5 { 672 if funct7 == 0x20 { return NX_RV64IM_ALU_SRA } 673 return NX_RV64IM_ALU_SRL 674 } 675 if funct3 == 6 { return NX_RV64IM_ALU_OR } 676 if funct3 == 7 { return NX_RV64IM_ALU_AND } 677 } 678 if op_kind == NX_RV64IM_OP_OP_IMM { 679 // Same as OP except immediates; ALU is the same compute layer 680 // (the executor swaps operand b for the immediate value). 681 if funct3 == 0 { return NX_RV64IM_ALU_ADD } 682 if funct3 == 1 { return NX_RV64IM_ALU_SLL } 683 if funct3 == 2 { return NX_RV64IM_ALU_SLT } 684 if funct3 == 3 { return NX_RV64IM_ALU_SLTU } 685 if funct3 == 4 { return NX_RV64IM_ALU_XOR } 686 if funct3 == 5 { 687 // SRLI vs SRAI distinguished by funct7 bit 30 688 if funct7 == 0x20 { return NX_RV64IM_ALU_SRA } 689 return NX_RV64IM_ALU_SRL 690 } 691 if funct3 == 6 { return NX_RV64IM_ALU_OR } 692 if funct3 == 7 { return NX_RV64IM_ALU_AND } 693 } 694 if op_kind == NX_RV64IM_OP_OP_32 { 695 if funct7 == 0x01 { 696 if funct3 == 0 { return NX_RV64IM_ALU_MULW } 697 if funct3 == 4 { return NX_RV64IM_ALU_DIVW } 698 if funct3 == 5 { return NX_RV64IM_ALU_DIVUW } 699 if funct3 == 6 { return NX_RV64IM_ALU_REMW } 700 if funct3 == 7 { return NX_RV64IM_ALU_REMUW } 701 } 702 if funct3 == 0 { 703 if funct7 == 0x20 { return NX_RV64IM_ALU_SUBW } 704 return NX_RV64IM_ALU_ADDW 705 } 706 if funct3 == 1 { return NX_RV64IM_ALU_SLLW } 707 if funct3 == 5 { 708 if funct7 == 0x20 { return NX_RV64IM_ALU_SRAW } 709 return NX_RV64IM_ALU_SRLW 710 } 711 } 712 if op_kind == NX_RV64IM_OP_OP_IMM_32 { 713 if funct3 == 0 { return NX_RV64IM_ALU_ADDW } 714 if funct3 == 1 { return NX_RV64IM_ALU_SLLW } 715 if funct3 == 5 { 716 if funct7 == 0x20 { return NX_RV64IM_ALU_SRAW } 717 return NX_RV64IM_ALU_SRLW 718 } 719 } 720 return NX_RV64IM_ALU_INVALID 721} 722 723// ===== Trap entry ================================================= 724// 725// Common path for all exceptions + interrupts. Saves mepc, sets 726// mcause, transitions mstatus per riscv-privileged-spec ยง3.1.6.2: 727// mstatus.MPIE <- mstatus.MIE (save the interrupt-enable bit) 728// mstatus.MIE <- 0 (disable interrupts in handler) 729// mstatus.MPP <- current privilege (M-mode in Tier A) 730// mepc <- pc-of-faulting-instruction 731// pc <- mtvec (direct mode; vectored mode unused) 732 733const NX_MCAUSE_INT_MACHINE_TIMER: i64 = 0 - 9223372036854775801 // (1<<63)|7 734const NX_MCAUSE_EXC_ECALL_U: i64 = 8 // environment call from U-mode 735const NX_MCAUSE_EXC_ECALL_S: i64 = 9 // environment call from S-mode 736const NX_MCAUSE_EXC_ECALL_M: i64 = 11 737const NX_MCAUSE_EXC_ILLEGAL_INSTR: i64 = 2 738const NX_MCAUSE_EXC_INSTR_PAGE_FAULT: i64 = 12 // Sv39 instruction-FETCH walk fault 739const NX_MCAUSE_EXC_LOAD_PAGE_FAULT: i64 = 13 // Sv39 data-load walk fault 740const NX_MCAUSE_EXC_STORE_PAGE_FAULT: i64 = 15 // Sv39 data-store walk fault 741 742const NX_MSTATUS_MIE_BIT: i64 = 0x08 // bit 3 743const NX_MSTATUS_MPIE_BIT: i64 = 0x80 // bit 7 744const NX_MSTATUS_MPP_MASK: i64 = 0x1800 // bits 11..12 745const NX_MIE_MTIE_BIT: i64 = 0x80 // bit 7 746 747func nx_rv64im_sim_take_trap(s: *NxRv64imSim, cause: i64, mepc_val: i64) -> i64 { 748 // S-MODE DELEGATION (the real OS trap path): an EXCEPTION (cause>=0, not an interrupt) taken from 749 // S/U-mode (priv<3) whose medeleg bit is set traps to S-mode -- stvec/sepc/scause/sstatus.SPP -- so 750 // the kernel handles its own page-faults/syscalls in S-mode and sret's back. medeleg resets to 0 751 // (mmap-zeroed) so with no delegation every trap still goes to M-mode exactly as before (no regression). 752 if cause >= 0 { if s.priv < 3 { 753 let medeleg: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MEDELEG) 754 if ((medeleg >> cause) & 1) == 1 { 755 let sst: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS) 756 var nsst: i64 = sst & (0 - 1 ^ 0x100) // clear SPP (mstatus bit 8) 757 nsst = nsst | ((s.priv & 1) << 8) // SPP <- the privilege we trap FROM (S=1 / U=0) 758 nx_rv64im_csr_write(s.csr, NX_CSR_MSTATUS, nsst) 759 nx_rv64im_csr_write(s.csr, NX_CSR_SEPC, mepc_val) 760 nx_rv64im_csr_write(s.csr, NX_CSR_SCAUSE, cause) 761 s.priv = 1 // enter S-mode (the kernel) 762 let stvec: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_STVEC) 763 s.pc = stvec & (0 - 4) 764 return NX_HDL_OK 765 } 766 } } 767 let mstatus: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS) 768 var new_mstatus: i64 = mstatus 769 // Save MIE into MPIE. 770 if (mstatus & NX_MSTATUS_MIE_BIT) != 0 { 771 new_mstatus = new_mstatus | NX_MSTATUS_MPIE_BIT 772 } 773 if (mstatus & NX_MSTATUS_MIE_BIT) == 0 { 774 new_mstatus = new_mstatus & (0 - 1 ^ NX_MSTATUS_MPIE_BIT) 775 } 776 // Disable interrupts. 777 new_mstatus = new_mstatus & (0 - 1 ^ NX_MSTATUS_MIE_BIT) 778 // MPP <- the privilege we are trapping FROM (so mret can restore it); then enter M-mode. 779 new_mstatus = (new_mstatus & (0 - 1 ^ NX_MSTATUS_MPP_MASK)) | ((s.priv & 0x3) << 11) 780 nx_rv64im_csr_write(s.csr, NX_CSR_MSTATUS, new_mstatus) 781 nx_rv64im_csr_write(s.csr, NX_CSR_MEPC, mepc_val) 782 nx_rv64im_csr_write(s.csr, NX_CSR_MCAUSE, cause) 783 s.priv = 3 // the handler runs in M-mode 784 // Jump to handler. Direct mode: low 2 bits of mtvec are 00. 785 let mtvec: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MTVEC) 786 s.pc = mtvec & (0 - 4) // clear low 2 bits 787 return NX_HDL_OK 788} 789 790func nx_rv64im_sim_mret(s: *NxRv64imSim) -> i64 { 791 let mstatus: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS) 792 var new_mstatus: i64 = mstatus 793 // MIE <- MPIE 794 if (mstatus & NX_MSTATUS_MPIE_BIT) != 0 { 795 new_mstatus = new_mstatus | NX_MSTATUS_MIE_BIT 796 } 797 if (mstatus & NX_MSTATUS_MPIE_BIT) == 0 { 798 new_mstatus = new_mstatus & (0 - 1 ^ NX_MSTATUS_MIE_BIT) 799 } 800 // MPIE <- 1 801 new_mstatus = new_mstatus | NX_MSTATUS_MPIE_BIT 802 // privilege <- MPP (the saved mode); then MPP <- U (0), per riscv-priv-spec mret. 803 let mpp: i64 = (mstatus >> 11) & 0x3 804 new_mstatus = new_mstatus & (0 - 1 ^ NX_MSTATUS_MPP_MASK) 805 nx_rv64im_csr_write(s.csr, NX_CSR_MSTATUS, new_mstatus) 806 s.priv = mpp 807 s.pc = nx_rv64im_csr_read(s.csr, NX_CSR_MEPC) 808 return NX_HDL_OK 809} 810 811// S-mode trap return -- the S-mode twin of mret (riscv-priv-spec): privilege <- sstatus.SPP (mstatus 812// bit 8: 1=S / 0=U); pc <- sepc; then SPP <- U. Lets an S-mode kernel return to the trapped context. 813func nx_rv64im_sim_sret(s: *NxRv64imSim) -> i64 { 814 let sst: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS) 815 let spp: i64 = (sst >> 8) & 1 816 let nsst: i64 = sst & (0 - 1 ^ 0x100) // SPP <- U(0) after sret 817 nx_rv64im_csr_write(s.csr, NX_CSR_MSTATUS, nsst) 818 s.priv = spp 819 s.pc = nx_rv64im_csr_read(s.csr, NX_CSR_SEPC) 820 return NX_HDL_OK 821} 822 823// ===== CSR address extraction ================================================= 824// 825// SYSTEM-op CSR address is inst[31:20] treated as UNSIGNED 12-bit. 826// Note this is the same bit slice as imm_i but NOT sign-extended. 827 828func nx_rv64im_csr_addr(inst: i64) -> i64 { 829 return (inst >> 20) & 0xfff 830} 831 832// ===== Single-cycle step ================================================= 833// 834// Executes ONE instruction at PC, advances PC + steps counter, ticks 835// CLINT once. Returns NX_HDL_OK on success, 0 - <verdict> on halt. 836 837func nx_rv64im_sim_step(s: *NxRv64imSim) -> i64 { 838 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND } 839 if s.halted == 1 { return 0 - NX_HDL_BAD_KIND } 840 841 // Tick CLINT first so MTIME advances per cycle (kernel polls it). 842 nx_clint_tick(s.clint) 843 nx_rv64im_csr_tick_mcycle(s.csr) 844 845 // Per-PC cycle attribution (M2 silicon-feedback foundation). Bumps 846 // the slot indexed by the executing PC; single branch when disabled. 847 if s.pc_cycles_on == 1 { 848 if s.pc >= s.mem_base { 849 let _attr_off: i64 = (s.pc - s.mem_base) >> 2 850 if _attr_off < s.pc_cycles_cap { 851 s.pc_cycles_buf[_attr_off] = s.pc_cycles_buf[_attr_off] + 1 852 } 853 } 854 } 855 856 // Interrupt check. CLINT asserts MTIP via nx_clint_tick; CPU 857 // takes the timer trap if mstatus.MIE && mie.MTIE. This is the 858 // path that fires kernel preemption every TICK_CYCLES. 859 if nx_clint_mtip_get(s.clint) == 1 { 860 let mstatus: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS) 861 let mie: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MIE) 862 if (mstatus & NX_MSTATUS_MIE_BIT) != 0 { 863 if (mie & NX_MIE_MTIE_BIT) != 0 { 864 nx_rv64im_sim_take_trap(s, NX_MCAUSE_INT_MACHINE_TIMER, s.pc) 865 s.steps = s.steps + 1 866 return NX_HDL_OK 867 } 868 } 869 } 870 871 // Fetch -- the PC is a VIRTUAL address in S/U-mode under Sv39, so the instruction fetch translates 872 // through the page table just like a data access (nx_rv64im_xlate returns identity in M-mode and 873 // under Bare, so every physically-addressed test -- M-mode boot, handlers, Bare programs -- is 874 // unchanged). A walk fault on the code page raises an instruction-page-fault (mcause=12) and 875 // aborts the fetch, vectoring to mtvec (where the M-mode handler is fetched physically). 876 s.xlate_fault = 0 877 let fetch_pa: i64 = nx_rv64im_xlate(s, s.pc, NX_ACC_FETCH) // fetch enforces the X bit 878 if s.xlate_fault == 1 { // code-page walk fault -> instruction-page-fault trap 879 nx_rv64im_csr_write(s.csr, NX_CSR_MTVAL, s.pc) 880 nx_rv64im_sim_take_trap(s, NX_MCAUSE_EXC_INSTR_PAGE_FAULT, s.pc) 881 s.steps = s.steps + 1 882 return NX_HDL_OK 883 } 884 // C extension: read a halfword; if compressed (low 2 bits != 11) expand it to the 32-bit equivalent + advance pc by 885 // 2, else read the full 32-bit word + advance by 4. TRANSPARENT to 32-bit code (low bits 11 -> not compressed). 886 // Fetch the code word ONCE and reuse it. Was two load32 calls per 32-bit instruction (the low 887 // 16 bits drive the compressed-check; the full 32 bits are the instruction) -- a redundant 888 // memory read every step. JIT-ladder rung 0: eliminate it. Behaviour-identical. 889 let fetch_word: i64 = nx_rv64im_sim_load32(s, fetch_pa) & 0xffffffff 890 let half: i64 = fetch_word & 0xffff 891 var inst: i64 = 0 892 var instr_len: i64 = 4 893 if nx_rvc_is_compressed(half) == 1 { inst = nx_rvc_expand(half); instr_len = 2 } 894 else { inst = fetch_word } 895 if inst == 0 { 896 // All-zero instruction: treat as halt sentinel. Kernel never 897 // emits these; real RV64 would raise illegal-instr. V1 sim 898 // uses this as a clean test-end signal. 899 s.halted = 1 900 s.halt_code = nx_rv64im_rf_read(s.rf, 10) // exit code in a0 901 return 0 902 } 903 904 // Decode 905 let op_kind: i64 = nx_rv64im_decode_kind(inst) 906 let rd: i64 = nx_rv64im_rd(inst) 907 let rs1: i64 = nx_rv64im_rs1(inst) 908 let rs2: i64 = nx_rv64im_rs2(inst) 909 let funct3: i64 = nx_rv64im_funct3(inst) 910 let funct7: i64 = nx_rv64im_funct7(inst) 911 912 let rs1_val: i64 = nx_rv64im_rf_read(s.rf, rs1) 913 let rs2_val: i64 = nx_rv64im_rf_read(s.rf, rs2) 914 915 var next_pc: i64 = s.pc + instr_len // C ext: +2 for compressed, +4 for 32-bit 916 917 if op_kind == NX_RV64IM_OP_LUI { 918 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_imm_u(inst)) 919 } 920 if op_kind == NX_RV64IM_OP_AUIPC { 921 nx_rv64im_rf_write(s.rf, rd, s.pc + nx_rv64im_imm_u(inst)) 922 } 923 if op_kind == NX_RV64IM_OP_JAL { 924 nx_rv64im_rf_write(s.rf, rd, s.pc + instr_len) 925 next_pc = s.pc + nx_rv64im_imm_j(inst) 926 } 927 if op_kind == NX_RV64IM_OP_JALR { 928 let tgt: i64 = (rs1_val + nx_rv64im_imm_i(inst)) & (0 - 2) 929 nx_rv64im_rf_write(s.rf, rd, s.pc + instr_len) 930 next_pc = tgt 931 } 932 if op_kind == NX_RV64IM_OP_BRANCH { 933 var take: i64 = 0 934 if funct3 == 0 { if rs1_val == rs2_val { take = 1 } } // beq 935 if funct3 == 1 { if rs1_val != rs2_val { take = 1 } } // bne 936 if funct3 == 4 { if rs1_val < rs2_val { take = 1 } } // blt 937 if funct3 == 5 { if rs1_val >= rs2_val { take = 1 } } // bge 938 if funct3 == 6 { take = nx_rv64im_ltu(rs1_val, rs2_val) } // bltu 939 if funct3 == 7 { 940 let lt: i64 = nx_rv64im_ltu(rs1_val, rs2_val) 941 if lt == 0 { take = 1 } // bgeu 942 } 943 if take == 1 { next_pc = s.pc + nx_rv64im_imm_b(inst) } 944 } 945 if op_kind == NX_RV64IM_OP_LOAD { 946 let vaddr: i64 = rs1_val + nx_rv64im_imm_i(inst) 947 s.xlate_fault = 0 948 let addr: i64 = nx_rv64im_xlate(s, vaddr, NX_ACC_LOAD) // Sv39 data translation + R enforcement 949 if s.xlate_fault == 1 { // walk/perm fault -> load-page-fault trap (instr aborted) 950 nx_rv64im_csr_write(s.csr, NX_CSR_MTVAL, vaddr) 951 nx_rv64im_sim_take_trap(s, NX_MCAUSE_EXC_LOAD_PAGE_FAULT, s.pc) 952 s.steps = s.steps + 1 953 return NX_HDL_OK 954 } 955 if funct3 == 0 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sext8(nx_rv64im_sim_load8(s, addr))) } // lb 956 if funct3 == 1 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sext16(nx_rv64im_sim_load16(s, addr))) } // lh 957 if funct3 == 2 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sext32(nx_rv64im_sim_load32(s, addr))) } // lw 958 if funct3 == 3 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sim_load64(s, addr)) } // ld 959 if funct3 == 4 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sim_load8(s, addr) & 0xff) } // lbu 960 if funct3 == 5 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sim_load16(s, addr) & 0xffff) } // lhu 961 if funct3 == 6 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sim_load32(s, addr) & 0xffffffff) } // lwu 962 } 963 if op_kind == NX_RV64IM_OP_STORE { 964 let vaddr: i64 = rs1_val + nx_rv64im_imm_s(inst) 965 s.xlate_fault = 0 966 let addr: i64 = nx_rv64im_xlate(s, vaddr, NX_ACC_STORE) // Sv39 data translation + W enforcement 967 if s.xlate_fault == 1 { // walk/perm fault -> store-page-fault trap (instr aborted) 968 nx_rv64im_csr_write(s.csr, NX_CSR_MTVAL, vaddr) 969 nx_rv64im_sim_take_trap(s, NX_MCAUSE_EXC_STORE_PAGE_FAULT, s.pc) 970 s.steps = s.steps + 1 971 return NX_HDL_OK 972 } 973 if funct3 == 0 { nx_rv64im_sim_store8(s, addr, rs2_val) } // sb (UART THR + general) 974 if funct3 == 1 { nx_rv64im_sim_store16(s, addr, rs2_val) } // sh 975 if funct3 == 2 { nx_rv64im_sim_store32(s, addr, rs2_val) } // sw (+ finisher 0x100000) 976 if funct3 == 3 { nx_rv64im_sim_store64(s, addr, rs2_val) } // sd 977 } 978 if op_kind == NX_RV64IM_OP_OP { 979 let alu_op: i64 = nx_rv64im_sim_alu_select(op_kind, funct3, funct7) 980 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val)) 981 } 982 if op_kind == NX_RV64IM_OP_OP_IMM { 983 let alu_op: i64 = nx_rv64im_sim_alu_select(op_kind, funct3, funct7) 984 let imm: i64 = nx_rv64im_imm_i(inst) 985 // SLLI/SRLI/SRAI shamt is encoded in the immediate's low 6 bits. 986 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, imm)) 987 } 988 if op_kind == NX_RV64IM_OP_OP_32 { 989 let alu_op: i64 = nx_rv64im_sim_alu_select(op_kind, funct3, funct7) 990 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val)) 991 } 992 // M-extension R-type ops (mul/div/rem family). The decoder gives 993 // them their own op_kind (OP_M_MUL / OP_M_DIV / *_32); alu_select 994 // keys on the base OP / OP_32 kind + funct7=0x01, so route through 995 // those. Without these four cases the sim silently skipped every 996 // multiply/divide/remainder and left rd unwritten. 997 if op_kind == NX_RV64IM_OP_M_MUL { 998 let alu_op: i64 = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP, funct3, funct7) 999 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val)) 1000 } 1001 if op_kind == NX_RV64IM_OP_M_DIV { 1002 let alu_op: i64 = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP, funct3, funct7) 1003 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val)) 1004 } 1005 if op_kind == NX_RV64IM_OP_M_MUL_32 { 1006 let alu_op: i64 = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP_32, funct3, funct7) 1007 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val)) 1008 } 1009 if op_kind == NX_RV64IM_OP_M_DIV_32 { 1010 let alu_op: i64 = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP_32, funct3, funct7) 1011 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val)) 1012 } 1013 if op_kind == NX_RV64IM_OP_OP_IMM_32 { 1014 let alu_op: i64 = nx_rv64im_sim_alu_select(op_kind, funct3, funct7) 1015 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, nx_rv64im_imm_i(inst))) 1016 } 1017 if op_kind == NX_RV64IM_OP_FENCE { 1018 // No-op in single-cycle in-order sim (no memory reordering). 1019 } 1020 if op_kind == NX_RV64IM_OP_SYSTEM { 1021 let csr_addr: i64 = nx_rv64im_csr_addr(inst) 1022 1023 if funct3 == 0 { 1024 // ecall / ebreak / mret -- distinguished by inst[31:20]. 1025 // Per spec table 3.4 / 9.1: 1026 // ecall -> imm[11:0] = 000000000000 1027 // ebreak -> imm[11:0] = 000000000001 1028 // mret -> imm[11:0] = 001100000010 (0x302) 1029 if csr_addr == 0x000 { 1030 // ecall -- the cause depends on the originating privilege (8=U, 9=S, 11=M); the 1031 // handler at mtvec is the kernel's syscall dispatcher. 1032 var ecause: i64 = NX_MCAUSE_EXC_ECALL_M 1033 if s.priv == 0 { ecause = NX_MCAUSE_EXC_ECALL_U } 1034 if s.priv == 1 { ecause = NX_MCAUSE_EXC_ECALL_S } 1035 nx_rv64im_sim_take_trap(s, ecause, s.pc) 1036 s.steps = s.steps + 1 1037 return NX_HDL_OK 1038 } 1039 if csr_addr == 0x001 { 1040 // ebreak -- treat as halt for the V1 sim (kernel 1041 // never executes ebreak in normal flow). 1042 s.halted = 1 1043 s.halt_code = nx_rv64im_rf_read(s.rf, 10) 1044 return 0 1045 } 1046 if csr_addr == 0x302 { 1047 // mret -- pc <- mepc, restore MIE from MPIE. 1048 nx_rv64im_sim_mret(s) 1049 s.steps = s.steps + 1 1050 return NX_HDL_OK 1051 } 1052 if csr_addr == 0x102 { 1053 // sret -- S-mode trap return: pc <- sepc, privilege <- sstatus.SPP. 1054 nx_rv64im_sim_sret(s) 1055 s.steps = s.steps + 1 1056 return NX_HDL_OK 1057 } 1058 // Unrecognised SYSTEM/funct3=0 op -- illegal. 1059 nx_rv64im_sim_take_trap(s, NX_MCAUSE_EXC_ILLEGAL_INSTR, s.pc) 1060 s.steps = s.steps + 1 1061 return NX_HDL_OK 1062 } 1063 1064 // csrr* family. Build the "operand" -- register value for 1065 // funct3 = 1/2/3; zero-extended 5-bit immediate for 5/6/7. 1066 var operand: i64 = 0 1067 if funct3 == 1 { operand = rs1_val } 1068 if funct3 == 2 { operand = rs1_val } 1069 if funct3 == 3 { operand = rs1_val } 1070 if funct3 == 5 { operand = rs1 & 0x1f } // csrrwi uimm = rs1 field 1071 if funct3 == 6 { operand = rs1 & 0x1f } 1072 if funct3 == 7 { operand = rs1 & 0x1f } 1073 1074 let old_out: *i64 = (sys_mmap(8)) as *i64 1075 old_out[0] = 0 1076 let cur: i64 = nx_rv64im_csr_read(s.csr, csr_addr) 1077 1078 if funct3 == 1 { // csrrw 1079 nx_rv64im_csr_swap(s.csr, csr_addr, operand, old_out) 1080 nx_rv64im_rf_write(s.rf, rd, old_out[0]) 1081 } 1082 if funct3 == 5 { // csrrwi 1083 nx_rv64im_csr_swap(s.csr, csr_addr, operand, old_out) 1084 nx_rv64im_rf_write(s.rf, rd, old_out[0]) 1085 } 1086 if funct3 == 2 { // csrrs -- set bits indicated by operand 1087 nx_rv64im_csr_write(s.csr, csr_addr, cur | operand) 1088 nx_rv64im_rf_write(s.rf, rd, cur) 1089 } 1090 if funct3 == 6 { // csrrsi 1091 nx_rv64im_csr_write(s.csr, csr_addr, cur | operand) 1092 nx_rv64im_rf_write(s.rf, rd, cur) 1093 } 1094 if funct3 == 3 { // csrrc -- clear bits indicated by operand 1095 nx_rv64im_csr_write(s.csr, csr_addr, cur & (0 - 1 ^ operand)) 1096 nx_rv64im_rf_write(s.rf, rd, cur) 1097 } 1098 if funct3 == 7 { // csrrci 1099 nx_rv64im_csr_write(s.csr, csr_addr, cur & (0 - 1 ^ operand)) 1100 nx_rv64im_rf_write(s.rf, rd, cur) 1101 } 1102 } 1103 if op_kind == NX_RV64IM_OP_AMO { 1104 // RV64A atomics: lr/sc + amo* (F107h). funct5 = inst[31:27] selects the op; funct3 = 2 (.w, 1105 // 32-bit) or 3 (.d, 64-bit). The address is rs1 EXACTLY (no immediate offset, per spec). 1106 let funct5: i64 = (inst >> 27) & 0x1f 1107 var is_d: i64 = 0 1108 if funct3 == 3 { is_d = 1 } 1109 let amo_va: i64 = rs1_val 1110 s.xlate_fault = 0 1111 let amo_pa: i64 = nx_rv64im_xlate(s, amo_va, NX_ACC_STORE) // amo needs R+W; enforce W (identity in M/Bare) 1112 if s.xlate_fault == 1 { 1113 nx_rv64im_csr_write(s.csr, NX_CSR_MTVAL, amo_va) 1114 nx_rv64im_sim_take_trap(s, NX_MCAUSE_EXC_STORE_PAGE_FAULT, s.pc) 1115 s.steps = s.steps + 1 1116 return NX_HDL_OK 1117 } 1118 // old value: sign-extended 32-bit for .w (so rd + signed compares are correct); full 64 for .d. 1119 var amo_old: i64 = 0 1120 if is_d == 1 { amo_old = nx_rv64im_sim_load64(s, amo_pa) } 1121 if is_d == 0 { amo_old = nx_rv64im_sext32(nx_rv64im_sim_load32(s, amo_pa)) } 1122 1123 if funct5 == 0x02 { 1124 // lr.w/d: record the reservation; rd <- old. 1125 s.resv_addr = amo_pa 1126 s.resv_valid = 1 1127 nx_rv64im_rf_write(s.rf, rd, amo_old) 1128 } 1129 if funct5 == 0x03 { 1130 // sc.w/d: success iff the reservation is still valid AND matches this address. 1131 var sc_ok: i64 = 0 1132 if s.resv_valid == 1 { if s.resv_addr == amo_pa { sc_ok = 1 } } 1133 if sc_ok == 1 { 1134 if is_d == 1 { nx_rv64im_sim_store64(s, amo_pa, rs2_val) } 1135 if is_d == 0 { nx_rv64im_sim_store32(s, amo_pa, rs2_val & 0xffffffff) } 1136 nx_rv64im_rf_write(s.rf, rd, 0) // 0 = success 1137 } 1138 if sc_ok == 0 { nx_rv64im_rf_write(s.rf, rd, 1) } // 1 = failure 1139 s.resv_valid = 0 // sc always clears the reservation 1140 } 1141 // amo* arithmetic/logical/min-max family (every funct5 except lr=0x02 / sc=0x03). 1142 if funct5 != 0x02 { if funct5 != 0x03 { 1143 // second operand: full 64 for .d; sign-extended low-32 for .w. 1144 var amo_src: i64 = rs2_val 1145 if is_d == 0 { amo_src = nx_rv64im_sext32(rs2_val & 0xffffffff) } 1146 var amo_new: i64 = amo_old 1147 if funct5 == 0x01 { amo_new = amo_src } // amoswap 1148 if funct5 == 0x00 { amo_new = amo_old + amo_src } // amoadd 1149 if funct5 == 0x04 { amo_new = amo_old ^ amo_src } // amoxor 1150 if funct5 == 0x0c { amo_new = amo_old & amo_src } // amoand 1151 if funct5 == 0x08 { amo_new = amo_old | amo_src } // amoor 1152 if funct5 == 0x10 { if amo_old < amo_src { amo_new = amo_old } if amo_old >= amo_src { amo_new = amo_src } } // amomin (signed) 1153 if funct5 == 0x14 { if amo_old < amo_src { amo_new = amo_src } if amo_old >= amo_src { amo_new = amo_old } } // amomax (signed) 1154 if funct5 == 0x18 { // amominu (unsigned) 1155 var uo: i64 = amo_old 1156 var us: i64 = amo_src 1157 if is_d == 0 { uo = amo_old & 0xffffffff; us = amo_src & 0xffffffff } 1158 if nx_rv64im_ltu(uo, us) == 1 { amo_new = amo_old } 1159 if nx_rv64im_ltu(uo, us) == 0 { amo_new = amo_src } 1160 } 1161 if funct5 == 0x1c { // amomaxu (unsigned) 1162 var uo2: i64 = amo_old 1163 var us2: i64 = amo_src 1164 if is_d == 0 { uo2 = amo_old & 0xffffffff; us2 = amo_src & 0xffffffff } 1165 if nx_rv64im_ltu(uo2, us2) == 1 { amo_new = amo_src } 1166 if nx_rv64im_ltu(uo2, us2) == 0 { amo_new = amo_old } 1167 } 1168 s.resv_valid = 0 // an amo writes the line -> breaks any reservation 1169 if is_d == 1 { nx_rv64im_sim_store64(s, amo_pa, amo_new) } 1170 if is_d == 0 { nx_rv64im_sim_store32(s, amo_pa, amo_new & 0xffffffff) } 1171 nx_rv64im_rf_write(s.rf, rd, amo_old) 1172 } } 1173 } 1174 if op_kind == NX_RV64IM_OP_INVALID { 1175 // Illegal instruction. Real kernel: trap with mcause=2. 1176 // V1 sim: halt loud. 1177 s.halted = 1 1178 s.halt_code = 0 - 2 1179 return 0 - NX_HDL_BAD_KIND 1180 } 1181 1182 s.pc = next_pc 1183 s.steps = s.steps + 1 1184 return NX_HDL_OK 1185} 1186 1187// ===== Run loop ================================================= 1188// 1189// Steps until halted or max_steps reached (guard against infinite 1190// loops in misbehaving test programs). Returns the final exit code 1191// from the halt path. 1192 1193func nx_rv64im_sim_run(s: *NxRv64imSim, max_steps: i64) -> i64 { 1194 var i: i64 = 0 1195 while i < max_steps { 1196 if s.halted == 1 { return s.halt_code } 1197 nx_rv64im_sim_step(s) 1198 i = i + 1 1199 } 1200 return 0 - 1 // exceeded max_steps; not halted cleanly 1201} 1202 1203// ===== Deterministic snapshot / restore (F107i, 2026-07-19) ================================================= 1204// 1205// Save + restore the ARCHITECTURAL machine state (32 GPRs + pc/priv/reservation/step-counters + all of RAM) 1206// to/from a caller buffer. Because the sovereign emulator is bit-DETERMINISTIC (no floating point, no host 1207// nondeterminism -- the determinism exceed bet), a restore is EXACT: continuing from a restored snapshot is 1208// byte-identical to never having stopped. This is what QEMU needs icount + savevm to approximate; here it 1209// is free. (CSR/CLINT/UART device state is the documented follow-on rung; this covers register+memory- 1210// defined behaviour -- the common case.) Buffer must be >= NX_RV64IM_SNAP_HDR + mem_size bytes. 1211const NX_RV64IM_SNAP_HDR: i64 = 312 // (7 scalars + 32 GPRs) * 8 bytes 1212 1213func nx_snap_wr64(b: *u8, off: i64, v: i64) -> i64 { var i: i64=0; while i<8 { b[off+i]=((v>>(i*8))&0xff) as u8; i=i+1 } return 0 } 1214func nx_snap_rd64(b: *u8, off: i64) -> i64 { var v: i64=0; var i: i64=0; while i<8 { v=v|((b[off+i] as i64)<<(i*8)); i=i+1 } return v } 1215 1216func nx_rv64im_sim_snapshot(s: *NxRv64imSim, buf: *u8) -> i64 { 1217 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND } 1218 nx_snap_wr64(buf, 0, s.pc) 1219 nx_snap_wr64(buf, 8, s.priv) 1220 nx_snap_wr64(buf, 16, s.halted) 1221 nx_snap_wr64(buf, 24, s.halt_code) 1222 nx_snap_wr64(buf, 32, s.steps) 1223 nx_snap_wr64(buf, 40, s.resv_addr) 1224 nx_snap_wr64(buf, 48, s.resv_valid) 1225 var i: i64 = 0 1226 while i < 32 { nx_snap_wr64(buf, 56 + i*8, nx_rv64im_rf_read(s.rf, i)); i = i + 1 } 1227 var m: i64 = 0 1228 while m < s.mem_size { buf[NX_RV64IM_SNAP_HDR + m] = s.mem_buf[m]; m = m + 1 } 1229 return NX_RV64IM_SNAP_HDR + s.mem_size 1230} 1231 1232func nx_rv64im_sim_restore(s: *NxRv64imSim, buf: *u8) -> i64 { 1233 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND } 1234 s.pc = nx_snap_rd64(buf, 0) 1235 s.priv = nx_snap_rd64(buf, 8) 1236 s.halted = nx_snap_rd64(buf, 16) 1237 s.halt_code = nx_snap_rd64(buf, 24) 1238 s.steps = nx_snap_rd64(buf, 32) 1239 s.resv_addr = nx_snap_rd64(buf, 40) 1240 s.resv_valid = nx_snap_rd64(buf, 48) 1241 var i: i64 = 0 1242 while i < 32 { nx_rv64im_rf_write(s.rf, i, nx_snap_rd64(buf, 56 + i*8)); i = i + 1 } 1243 var m: i64 = 0 1244 while m < s.mem_size { s.mem_buf[m] = buf[NX_RV64IM_SNAP_HDR + m]; m = m + 1 } 1245 return NX_HDL_OK 1246}