code wiki / _hdl_build / rv64im_min_kernel_boot_smoke.nx
rv64im_min_kernel_boot_smoke.nx source
↩ module page · 120 lines · 5191 B
1// rv64im_min_kernel_boot_smoke.nx -- boot the nishi-os kernel ELF in sim.
2//
3// End-to-end test that closes the loop from
4// "shipped NishiHDL modules" -> "sim runs hand-coded RV64IM"
5// -> "ELF loader works" -> "ACTUAL KERNEL BOOTS in our pure-NishiLang
6// substrate, no qemu, no external interpreter"
7//
8// What this proves:
9// The 11 NishiHDL files under nishi-silicon/hdl/ collectively
10// implement enough of RV64IM-min that the shipped nishi-os
11// kernel ELF executes correctly: at minimum, the kernel's
12// selftest path prints the first [m0-mcs-live] UART marker
13// to our captured tx_buf within the step budget.
14//
15// What this DOES NOT prove (still needed for Tier A FPGA bring-up):
16// - nishi-synth gate-netlist emit (HDL graph -> Verilog/gates)
17// - Yosys + nextpnr place-and-route (gates -> ECP5 bitstream)
18// - FPGA board execution (bitstream -> ULX3S -> physical UART)
19//
20// Status: SEED. 2026-05-26. Initial harness. Step budget is
21// generous (10M) since the kernel runs many cycles before reaching
22// the first observable marker.
23
24import "nx_syscalls.nx"
25import "nishi_hdl_primitives.nx"
26import "rv64im_min_decoder.nx"
27import "rv64im_min_alu.nx"
28import "rv64im_min_regfile.nx"
29import "rv64im_min_csr.nx"
30import "rv64im_min_clint.nx"
31import "rv64im_min_uart.nx"
32import "rv64im_min_sim.nx"
33import "rv64im_min_elf_loader.nx"
34
35const BOOT_MEM_BASE: i64 = 0x80000000
36const BOOT_MEM_SIZE: i64 = 0x40000 // 256 KiB (kernel + stack + frames)
37const BOOT_TX_CAP: i64 = 4096 // capture up to 4 KiB of UART output
38const BOOT_MAX_STEPS: i64 = 10000000 // 10M cycle budget
39
40const BOOT_KERNEL_PATH: *u8 = "/mnt/c/Users/elder/nishi-os/kernel/nishi-kernel.elf"
41
42// Substring search: returns 1 if needle appears in haystack[0..hay_len].
43// Naive O(n*m) -- fine for our 4 KiB haystack + 15-char needle.
44func boot_contains(hay: *u8, hay_len: i64, needle: *u8, needle_len: i64) -> i64 {
45 if needle_len == 0 { return 1 }
46 if needle_len > hay_len { return 0 }
47 var i: i64 = 0
48 while i <= hay_len - needle_len {
49 var j: i64 = 0
50 var matched: i64 = 1
51 while j < needle_len {
52 if (hay[i + j] & 0xff) != (needle[j] & 0xff) { matched = 0; j = needle_len }
53 j = j + 1
54 }
55 if matched == 1 { return 1 }
56 i = i + 1
57 }
58 return 0
59}
60
61func main() -> i64 {
62 // ----- read the kernel ELF -----
63 let elf_len_out: *i64 = (sys_mmap(8)) as *i64
64 elf_len_out[0] = 0
65 let elf_bytes: *u8 = sys_read_file(BOOT_KERNEL_PATH, elf_len_out)
66 let elf_size: i64 = elf_len_out[0]
67 if (elf_bytes as i64) == 0 { return 1 } // file read failed
68 if elf_size < 64 { return 2 } // smaller than ELF64 header
69
70 // ----- allocate the simulated machine -----
71 let rf_storage: *i64 = (sys_mmap(8 * NX_RV64IM_RF_N_REGS)) as *i64
72 let csr_storage: *i64 = (sys_mmap(8 * NX_CSR_SLOT_N)) as *i64
73 let clint_storage: *i64 = (sys_mmap(8 * NX_CLINT_SLOT_N)) as *i64
74 let uart_storage: *i64 = (sys_mmap(8 * NX_UART_SLOT_N)) as *i64
75 let mem: *u8 = sys_mmap(BOOT_MEM_SIZE)
76 let tx_buf: *u8 = sys_mmap(BOOT_TX_CAP)
77
78 let rf: *NxRv64imRegfile = (sys_mmap(64)) as *NxRv64imRegfile
79 let csr: *NxRv64imCsrFile = (sys_mmap(64)) as *NxRv64imCsrFile
80 let clint: *NxClint = (sys_mmap(64)) as *NxClint
81 let uart: *NxUart = (sys_mmap(64)) as *NxUart
82 let sim: *NxRv64imSim = (sys_mmap(128)) as *NxRv64imSim
83
84 nx_rv64im_rf_init(rf, rf_storage)
85 nx_rv64im_csr_init(csr, csr_storage, 0)
86 nx_clint_init(clint, clint_storage)
87 nx_uart_init(uart, uart_storage, tx_buf, BOOT_TX_CAP)
88 nx_rv64im_sim_init(sim, rf, csr, clint, uart,
89 BOOT_MEM_BASE, mem, BOOT_MEM_SIZE, 0)
90
91 // ----- load the kernel ELF into sim memory -----
92 let elf_out: *NxElfLoad = (sys_mmap(64)) as *NxElfLoad
93 let rc: i64 = nx_elf_load(elf_bytes, elf_size, sim, elf_out)
94 if rc != NX_ELF_OK {
95 // Surface the negated verdict code via exit -- helps diagnostics.
96 return 10 + (0 - rc)
97 }
98 if elf_out.entry != BOOT_MEM_BASE { return 3 } // expect kernel entry = 0x80000000
99 if elf_out.n_pt_load < 1 { return 4 } // must have >= 1 PT_LOAD
100
101 // ----- run -----
102 nx_rv64im_sim_run(sim, BOOT_MAX_STEPS)
103
104 // ----- assertions -----
105 //
106 // We don't require sim.halted == 1 because the kernel may still
107 // be in its bench loop after the first marker prints; the
108 // selftest output is what we care about.
109 let tx_n: i64 = nx_uart_tx_count(uart)
110 if tx_n == 0 { return 5 } // no UART output at all
111
112 // The kernel's first observable marker is [m0-mcs-live] from
113 // sched.nx, which fires after MCS scheduler init. If it
114 // appears, we know decoder + ALU + regfile + CSR + UART all
115 // executed correctly through a substantial chunk of kernel boot.
116 let marker_mcs: *u8 = "[m0-mcs-live]"
117 if boot_contains(tx_buf, tx_n, marker_mcs, 13) != 1 { return 6 }
118
119 return 0 // GREEN -- kernel booted far enough to print [m0-mcs-live]
120}