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}