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}