code wiki / _hdl_build / rv64im_min_csr.nx

rv64im_min_csr.nx source

↩ module page · 270 lines · 11306 B

1// rv64im_min_csr.nx -- RV64IM-min CSR file (M-mode only). 2// 3// Implements the 7 MUST CSRs from hdl/rv64im_min_target_spec.md, 4// plus the 2 SHOULD CSRs that the kernel reads on the bench/diag 5// path. This is the smallest CSR set that lets the shipped 6// nishi-os kernel boot, take a timer interrupt, and return via mret. 7// 8// MUST (boot fails without): 9// mhartid (0xF14) read hart ID -- non-zero harts park at boot 10// mscratch (0x340) 64-bit RW; trap-frame pointer swap 11// mtvec (0x305) trap vector base (direct mode; low bits = 00) 12// mepc (0x341) exception PC saved/restored every trap 13// mstatus (0x300) bits 3 (MIE), 7 (MPIE), 11-12 (MPP) 14// mie (0x304) bit 7 (MTIE) for timer enable 15// mcause (0x342) bit 63 = int/exc; low 8 bits = code 16// 17// SHOULD (kernel degrades cleanly without): 18// mcycle (0xB00) 64-bit cycle counter; bench-only 19// mtval (0x343) exception value; panic diagnostics only 20// 21// Status: SEED. 2026-05-26. Storage + access semantics + sealed- 22// enum address dispatch. Updates are sequential (commit on clock 23// edge); reads are combinational so back-to-back csrr/csrw works. 24 25import "nx_syscalls.nx" 26import "nishi_hdl_primitives.nx" 27 28// ===== CSR addresses (per riscv-privileged-spec) ================================================= 29const NX_CSR_MSTATUS: i64 = 0x300 30const NX_CSR_MIE: i64 = 0x304 31const NX_CSR_MTVEC: i64 = 0x305 32const NX_CSR_MSCRATCH: i64 = 0x340 33const NX_CSR_MEPC: i64 = 0x341 34const NX_CSR_MCAUSE: i64 = 0x342 35const NX_CSR_MTVAL: i64 = 0x343 36const NX_CSR_MHARTID: i64 = 0xF14 37const NX_CSR_MCYCLE: i64 = 0xB00 38const NX_CSR_SATP: i64 = 0x180 // supervisor address-translation + protection (Sv39 paging) 39const NX_CSR_STVEC: i64 = 0x105 // S-mode trap vector (the kernel's own trap entry) 40const NX_CSR_SEPC: i64 = 0x141 // S-mode exception PC 41const NX_CSR_SCAUSE: i64 = 0x142 // S-mode trap cause 42const NX_CSR_MEDELEG: i64 = 0x302 // machine exception delegation -> bit c set = exception c handled in S-mode 43 44// ===== Storage index (sealed enum) ================================================= 45// 46// Each implemented CSR gets a contiguous storage slot so the storage 47// array is dense (no sparse 12-bit address indirection). The 48// dispatcher (nx_csr_addr_to_slot) maps RISC-V addresses to slots. 49const NX_CSR_SLOT_MSTATUS: i64 = 0 50const NX_CSR_SLOT_MIE: i64 = 1 51const NX_CSR_SLOT_MTVEC: i64 = 2 52const NX_CSR_SLOT_MSCRATCH: i64 = 3 53const NX_CSR_SLOT_MEPC: i64 = 4 54const NX_CSR_SLOT_MCAUSE: i64 = 5 55const NX_CSR_SLOT_MTVAL: i64 = 6 56const NX_CSR_SLOT_MHARTID: i64 = 7 57const NX_CSR_SLOT_MCYCLE: i64 = 8 58const NX_CSR_SLOT_SATP: i64 = 9 59const NX_CSR_SLOT_STVEC: i64 = 10 60const NX_CSR_SLOT_SEPC: i64 = 11 61const NX_CSR_SLOT_SCAUSE: i64 = 12 62const NX_CSR_SLOT_MEDELEG: i64 = 13 63const NX_CSR_SLOT_N: i64 = 14 64 65// ===== Verdicts ================================================= 66const NX_CSR_OK: i64 = 0 67const NX_CSR_UNIMPLEMENTED: i64 = 1 // CSR address not in MUST or SHOULD set 68const NX_CSR_READ_ONLY: i64 = 2 // write attempted to RO csr (mhartid, mcycle) 69 70// ===== Address -> slot dispatcher ================================================= 71// 72// Returns the slot index for an implemented CSR, or 0 - NX_CSR_UNIMPLEMENTED. 73// V1 hand-coded if-chain; future synth path emits a 12-bit decoder 74// gate net. 75 76func nx_csr_addr_to_slot(addr: i64) -> i64 { 77 if addr == NX_CSR_MSTATUS { return NX_CSR_SLOT_MSTATUS } 78 if addr == NX_CSR_MIE { return NX_CSR_SLOT_MIE } 79 if addr == NX_CSR_MTVEC { return NX_CSR_SLOT_MTVEC } 80 if addr == NX_CSR_MSCRATCH { return NX_CSR_SLOT_MSCRATCH } 81 if addr == NX_CSR_MEPC { return NX_CSR_SLOT_MEPC } 82 if addr == NX_CSR_MCAUSE { return NX_CSR_SLOT_MCAUSE } 83 if addr == NX_CSR_MTVAL { return NX_CSR_SLOT_MTVAL } 84 if addr == NX_CSR_MHARTID { return NX_CSR_SLOT_MHARTID } 85 if addr == NX_CSR_MCYCLE { return NX_CSR_SLOT_MCYCLE } 86 if addr == NX_CSR_SATP { return NX_CSR_SLOT_SATP } 87 if addr == NX_CSR_STVEC { return NX_CSR_SLOT_STVEC } 88 if addr == NX_CSR_SEPC { return NX_CSR_SLOT_SEPC } 89 if addr == NX_CSR_SCAUSE { return NX_CSR_SLOT_SCAUSE } 90 if addr == NX_CSR_MEDELEG { return NX_CSR_SLOT_MEDELEG } 91 return 0 - NX_CSR_UNIMPLEMENTED 92} 93 94// ===== Read-only mask ================================================= 95// 96// Per riscv-spec: mhartid is read-only; mcycle is read-only on 97// implementations that don't expose H-extension counter writeback. 98// Writes to these CSRs trap (illegal-instruction) on real silicon; 99// V1 simulator returns READ_ONLY verdict so the caller can raise 100// the exception. 101 102func nx_csr_is_read_only(slot: i64) -> i64 { 103 if slot == NX_CSR_SLOT_MHARTID { return 1 } 104 if slot == NX_CSR_SLOT_MCYCLE { return 1 } // RV64 spec: mcycle writable 105 // in privileged-spec §2.8, but 106 // Tier A FPGA treats it as 107 // free-running counter (RO). 108 return 0 109} 110 111// ===== Storage ================================================= 112// 113// V1: caller allocates 9 i64s for the slot table + 1 i64 for the 114// hart id constant. Substrate-side init stamps the initial values 115// per the riscv-privileged-spec reset state: 116// 117// mstatus.MIE = 0 (interrupts disabled at boot) 118// mstatus.MPP = M (post-mret to M-mode by default) 119// mie = 0 (no interrupts enabled) 120// mtvec = 0 (trap to address 0 unless kernel writes; kernel 121// immediately writes &trap_entry per boot.S) 122// mscratch = 0 123// mepc = 0 124// mcause = 0 125// mtval = 0 126// mhartid = caller-supplied (typically 0) 127// mcycle = 0 (free-running counter; advanced by clock domain) 128 129const NX_CSR_RESET_MSTATUS: i64 = 0x1800 // MPP=11 (M-mode), MIE=0, MPIE=0 130 131struct NxRv64imCsrFile { 132 storage: *i64 // 9 i64s 133 valid: i64 134} 135 136func nx_rv64im_csr_init(csr: *NxRv64imCsrFile, storage: *i64, hartid: i64) -> i64 { 137 if (csr as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 138 if (storage as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 139 csr.storage = storage 140 csr.valid = 1 141 storage[NX_CSR_SLOT_MSTATUS] = NX_CSR_RESET_MSTATUS 142 storage[NX_CSR_SLOT_MIE] = 0 143 storage[NX_CSR_SLOT_MTVEC] = 0 144 storage[NX_CSR_SLOT_MSCRATCH] = 0 145 storage[NX_CSR_SLOT_MEPC] = 0 146 storage[NX_CSR_SLOT_MCAUSE] = 0 147 storage[NX_CSR_SLOT_MTVAL] = 0 148 storage[NX_CSR_SLOT_MHARTID] = hartid 149 storage[NX_CSR_SLOT_MCYCLE] = 0 150 storage[NX_CSR_SLOT_SATP] = 0 // Bare (MODE=0) at reset: no translation -> no regression 151 return NX_CSR_OK 152} 153 154// ===== Combinational read ================================================= 155// 156// V1: 12-bit address -> slot dispatch -> i64 storage read. Returns 157// the CSR value on OK, or returns 0 with verdict signaled via 158// out-param (V1 keeps the read signature simple; the simulator 159// detects unimplemented CSRs at the addr_to_slot step). 160 161func nx_rv64im_csr_read(csr: *NxRv64imCsrFile, addr: i64) -> i64 { 162 if csr.valid != 1 { return 0 } 163 let slot: i64 = nx_csr_addr_to_slot(addr) 164 if slot < 0 { return 0 } // unimplemented; executor raises illegal-instr 165 return csr.storage[slot] 166} 167 168// ===== Sequential write ================================================= 169// 170// Commits on clock edge in silicon; commits immediately in V1 171// simulator path. Returns OK / UNIMPLEMENTED / READ_ONLY verdict. 172 173func nx_rv64im_csr_write(csr: *NxRv64imCsrFile, addr: i64, value: i64) -> i64 { 174 if csr.valid != 1 { return 0 - NX_CSR_UNIMPLEMENTED } 175 let slot: i64 = nx_csr_addr_to_slot(addr) 176 if slot < 0 { return slot } 177 if nx_csr_is_read_only(slot) == 1 { return 0 - NX_CSR_READ_ONLY } 178 csr.storage[slot] = value 179 return NX_CSR_OK 180} 181 182// ===== Atomic swap (csrrw) ================================================= 183// 184// RV64IM spec §9.1: csrrw reads the OLD value into rd, then writes 185// the new value. Atomic w.r.t. interrupts. 186 187func nx_rv64im_csr_swap(csr: *NxRv64imCsrFile, addr: i64, 188 new_value: i64, old_out: *i64) -> i64 { 189 if csr.valid != 1 { return 0 - NX_CSR_UNIMPLEMENTED } 190 if (old_out as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 191 let slot: i64 = nx_csr_addr_to_slot(addr) 192 if slot < 0 { return slot } 193 if nx_csr_is_read_only(slot) == 1 { 194 // csrrw to RO CSR: spec says read still succeeds, write is 195 // suppressed but does NOT trap (unlike csrw to a hardwired- 196 // zero CSR which doesn't trap either). V1 mirrors spec. 197 old_out[0] = csr.storage[slot] 198 return NX_CSR_OK 199 } 200 old_out[0] = csr.storage[slot] 201 csr.storage[slot] = new_value 202 return NX_CSR_OK 203} 204 205// ===== mcycle tick ================================================= 206// 207// Free-running cycle counter; the clock domain calls this once per 208// posedge of clk. In silicon, this is a simple 64-bit incrementer 209// fed from the fabric clock; in the V1 simulator the caller drives 210// it from the simulation step. 211 212func nx_rv64im_csr_tick_mcycle(csr: *NxRv64imCsrFile) -> i64 { 213 if csr.valid != 1 { return 0 - NX_CSR_UNIMPLEMENTED } 214 csr.storage[NX_CSR_SLOT_MCYCLE] = csr.storage[NX_CSR_SLOT_MCYCLE] + 1 215 return NX_CSR_OK 216} 217 218// ===== HDL-graph builder ================================================= 219 220const NX_CSR_WIDTH_ADDR: i64 = 12 // RV64 CSR address space is 12 bits 221const NX_CSR_WIDTH_VALUE: i64 = 64 222 223struct NxRv64imCsrFilePorts { 224 clk: i64 // clock 225 reset: i64 // sync reset 226 rd_addr: i64 // input wire, 12-bit 227 rd_data: i64 // output wire, 64-bit 228 wr_addr: i64 // input wire, 12-bit 229 wr_data: i64 // input wire, 64-bit 230 wr_en: i64 // input wire, 1-bit 231 wr_verdict: i64 // output wire, 4-bit (sealed enum verdict) 232} 233 234const NX_CSR_WIDTH_VERDICT: i64 = 4 235 236func nx_rv64im_csr_build(m: *NxHdlModule, ports: *NxRv64imCsrFilePorts) -> i64 { 237 let p_clk: i64 = nx_hdl_clock(m) 238 if p_clk < 0 { return p_clk } 239 let p_rst: i64 = nx_hdl_reset(m) 240 if p_rst < 0 { return p_rst } 241 242 let p_rd_addr: i64 = nx_hdl_input(m, NX_CSR_WIDTH_ADDR) 243 if p_rd_addr < 0 { return p_rd_addr } 244 let p_rd_data: i64 = nx_hdl_output(m, NX_CSR_WIDTH_VALUE) 245 if p_rd_data < 0 { return p_rd_data } 246 247 let p_wr_addr: i64 = nx_hdl_input(m, NX_CSR_WIDTH_ADDR) 248 if p_wr_addr < 0 { return p_wr_addr } 249 let p_wr_data: i64 = nx_hdl_input(m, NX_CSR_WIDTH_VALUE) 250 if p_wr_data < 0 { return p_wr_data } 251 let p_wr_en: i64 = nx_hdl_input(m, 1) 252 if p_wr_en < 0 { return p_wr_en } 253 let p_wr_verdict: i64 = nx_hdl_output(m, NX_CSR_WIDTH_VERDICT) 254 if p_wr_verdict < 0 { return p_wr_verdict } 255 256 ports.clk = p_clk 257 ports.reset = p_rst 258 ports.rd_addr = p_rd_addr 259 ports.rd_data = p_rd_data 260 ports.wr_addr = p_wr_addr 261 ports.wr_data = p_wr_data 262 ports.wr_en = p_wr_en 263 ports.wr_verdict = p_wr_verdict 264 265 // Synth target: 9 64-bit flop registers + 12-bit address decoder 266 // + multiplexer. Tiny in gate count (~600 gates) and routes 267 // trivially on FPGA fabric. Tier B+ MPW: same shape with custom 268 // standard-cell flops. 269 return NX_HDL_OK 270}