code wiki / _hdl_build / rv64im_min_clint.nx
rv64im_min_clint.nx source
↩ module page · 240 lines · 9256 B
1// rv64im_min_clint.nx -- Core-Local Interruptor (CLINT) MMIO device.
2//
3// Drives the kernel's preemptive timer. Two memory-mapped registers:
4//
5// MTIME (0x0200BFF8, 64-bit RO from CPU side)
6// Monotonic counter; advances 1 per fabric-clock tick. Kernel
7// reads it on every preempt-tick handler to measure latency.
8//
9// MTIMECMP[0] (0x02004000, 64-bit RW)
10// Per-hart compare register. When MTIME >= MTIMECMP[0], the
11// CLINT asserts MTIP on the hart; the CPU takes a machine-timer
12// interrupt (mcause = (1 << 63) | 7) if mstatus.MIE and mie.MTIE
13// are both set. Kernel rearms by writing MTIMECMP[0] = MTIME +
14// TICK_CYCLES.
15//
16// Tier A FPGA: single-hart only, so MTIMECMP[N] for N>0 is absent.
17// The full RISC-V CLINT spec also defines MSIP (software interrupt)
18// at 0x02000000, but the kernel never uses it -- omitted here per
19// the spec's "must implement enough that kernel boots" cardinal.
20//
21// Memory map (matches QEMU virt machine; kernel hardcodes these):
22// 0x02000000 MSIP[0] -- NOT IMPLEMENTED in Tier A (4 bytes)
23// 0x02004000 MTIMECMP[0] -- 8 bytes
24// 0x02004008..0x0200BFF7 sparse; reads/writes return 0
25// 0x0200BFF8 MTIME -- 8 bytes
26// 0x0200C000 end of region
27//
28// Status: SEED. 2026-05-26. Register storage + access + interrupt
29// generation. Synth target: 2 64-bit flop registers + comparator +
30// memory-region address decoder. Tier A FPGA: trivial.
31
32import "nx_syscalls.nx"
33import "nishi_hdl_primitives.nx"
34
35// ===== MMIO addresses =================================================
36const NX_CLINT_BASE: i64 = 0x02000000
37const NX_CLINT_MSIP_BASE: i64 = 0x02000000
38const NX_CLINT_MTIMECMP_BASE: i64 = 0x02004000
39const NX_CLINT_MTIME_ADDR: i64 = 0x0200BFF8
40const NX_CLINT_END: i64 = 0x0200C000
41
42// ===== Verdicts =================================================
43const NX_CLINT_OK: i64 = 0
44const NX_CLINT_ADDR_OUT_OF_RANGE: i64 = 1
45const NX_CLINT_BAD_ALIGNMENT: i64 = 2 // 8-byte access required
46
47// ===== Storage =================================================
48//
49// V1: caller allocates a 3-i64 backing buffer:
50// slot 0: MTIME (64-bit monotonic counter)
51// slot 1: MTIMECMP[0] (64-bit compare; init to INT_MAX so no
52// spurious interrupt before kernel programs it)
53// slot 2: MTIP latch (1-bit pending state; output to CPU's mip.MTIP)
54
55const NX_CLINT_SLOT_MTIME: i64 = 0
56const NX_CLINT_SLOT_MTIMECMP: i64 = 1
57const NX_CLINT_SLOT_MTIP: i64 = 2
58const NX_CLINT_SLOT_N: i64 = 3
59
60const NX_CLINT_INIT_MTIMECMP: i64 = 0x7FFFFFFFFFFFFFFF // INT64_MAX
61
62struct NxClint {
63 storage: *i64 // 3 i64s
64 valid: i64
65}
66
67func nx_clint_init(c: *NxClint, storage: *i64) -> i64 {
68 if (c as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
69 if (storage as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
70 c.storage = storage
71 c.valid = 1
72 storage[NX_CLINT_SLOT_MTIME] = 0
73 storage[NX_CLINT_SLOT_MTIMECMP] = NX_CLINT_INIT_MTIMECMP
74 storage[NX_CLINT_SLOT_MTIP] = 0
75 return NX_CLINT_OK
76}
77
78// ===== Range check =================================================
79func nx_clint_addr_in_range(addr: i64) -> i64 {
80 if addr < NX_CLINT_BASE { return 0 }
81 if addr >= NX_CLINT_END { return 0 }
82 return 1
83}
84
85// ===== MMIO read =================================================
86//
87// 64-bit aligned reads only. Returns the value via the out-param
88// + verdict via the return. Reads to unmapped offsets return 0
89// (matches QEMU virt behavior; kernel never reads them).
90
91func nx_clint_read64(c: *NxClint, addr: i64, value_out: *i64) -> i64 {
92 if c.valid != 1 { return 0 - NX_CLINT_ADDR_OUT_OF_RANGE }
93 if (value_out as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
94 if nx_clint_addr_in_range(addr) != 1 { return 0 - NX_CLINT_ADDR_OUT_OF_RANGE }
95 if (addr & 0x7) != 0 { return 0 - NX_CLINT_BAD_ALIGNMENT }
96
97 if addr == NX_CLINT_MTIME_ADDR {
98 value_out[0] = c.storage[NX_CLINT_SLOT_MTIME]
99 return NX_CLINT_OK
100 }
101 if addr == NX_CLINT_MTIMECMP_BASE {
102 value_out[0] = c.storage[NX_CLINT_SLOT_MTIMECMP]
103 return NX_CLINT_OK
104 }
105 // Other addresses in the CLINT region: return 0. Includes MSIP
106 // (not implemented; kernel never reads it).
107 value_out[0] = 0
108 return NX_CLINT_OK
109}
110
111// ===== MMIO write =================================================
112
113func nx_clint_write64(c: *NxClint, addr: i64, value: i64) -> i64 {
114 if c.valid != 1 { return 0 - NX_CLINT_ADDR_OUT_OF_RANGE }
115 if nx_clint_addr_in_range(addr) != 1 { return 0 - NX_CLINT_ADDR_OUT_OF_RANGE }
116 if (addr & 0x7) != 0 { return 0 - NX_CLINT_BAD_ALIGNMENT }
117
118 if addr == NX_CLINT_MTIMECMP_BASE {
119 c.storage[NX_CLINT_SLOT_MTIMECMP] = value
120 // Rearming may clear MTIP (if new compare > current MTIME)
121 // or set it (if new compare <= current MTIME). Recompute.
122 nx_clint_update_mtip(c)
123 return NX_CLINT_OK
124 }
125 if addr == NX_CLINT_MTIME_ADDR {
126 // RV64 spec: MTIME is writable in M-mode for diagnostic
127 // purposes. Kernel doesn't use this but we honor the spec.
128 c.storage[NX_CLINT_SLOT_MTIME] = value
129 nx_clint_update_mtip(c)
130 return NX_CLINT_OK
131 }
132 // Other addresses: silently dropped.
133 return NX_CLINT_OK
134}
135
136// ===== Pending-bit derivation =================================================
137//
138// MTIP = (MTIME >= MTIMECMP). Refreshed:
139// - every clock tick (via nx_clint_tick)
140// - after MTIMECMP write (via nx_clint_write64)
141// - after MTIME write (via nx_clint_write64)
142// CPU reads via nx_clint_mtip_get().
143
144func nx_clint_update_mtip(c: *NxClint) -> i64 {
145 if c.valid != 1 { return 0 - NX_CLINT_ADDR_OUT_OF_RANGE }
146 let now: i64 = c.storage[NX_CLINT_SLOT_MTIME]
147 let cmp: i64 = c.storage[NX_CLINT_SLOT_MTIMECMP]
148 // Unsigned compare via top-bit-flip trick (NishiLang < is signed).
149 let bit63: i64 = 0 - 9223372036854775808
150 let now_f: i64 = now ^ bit63
151 let cmp_f: i64 = cmp ^ bit63
152 if now_f < cmp_f {
153 c.storage[NX_CLINT_SLOT_MTIP] = 0
154 }
155 if now_f >= cmp_f {
156 c.storage[NX_CLINT_SLOT_MTIP] = 1
157 }
158 return NX_CLINT_OK
159}
160
161func nx_clint_mtip_get(c: *NxClint) -> i64 {
162 if c.valid != 1 { return 0 }
163 return c.storage[NX_CLINT_SLOT_MTIP]
164}
165
166// ===== Clock tick (advances MTIME) =================================================
167//
168// Driven by the simulator each cycle; in silicon this is a free-
169// running 64-bit counter fed from the fabric clock. Tier A FPGA:
170// MTIME advances at 10 MHz (kernel's TICK_CYCLES=1000 yields ~10 kHz
171// preemption).
172
173func nx_clint_tick(c: *NxClint) -> i64 {
174 if c.valid != 1 { return 0 - NX_CLINT_ADDR_OUT_OF_RANGE }
175 c.storage[NX_CLINT_SLOT_MTIME] = c.storage[NX_CLINT_SLOT_MTIME] + 1
176 return nx_clint_update_mtip(c)
177}
178
179// ===== HDL-graph builder =================================================
180
181const NX_CLINT_WIDTH_ADDR: i64 = 32 // physical address space
182const NX_CLINT_WIDTH_DATA: i64 = 64
183const NX_CLINT_WIDTH_VERDICT: i64 = 4
184const NX_CLINT_WIDTH_MTIP: i64 = 1
185
186struct NxClintPorts {
187 clk: i64 // fabric clock
188 reset: i64 // sync reset
189 rd_addr: i64 // input wire, 32-bit
190 rd_en: i64 // input wire, 1-bit
191 rd_data: i64 // output wire, 64-bit
192 rd_verdict: i64 // output wire, 4-bit
193 wr_addr: i64 // input wire, 32-bit
194 wr_data: i64 // input wire, 64-bit
195 wr_en: i64 // input wire, 1-bit
196 wr_verdict: i64 // output wire, 4-bit
197 mtip_out: i64 // output wire, 1-bit (feeds CPU's mip.MTIP)
198}
199
200func nx_clint_build(m: *NxHdlModule, ports: *NxClintPorts) -> i64 {
201 let p_clk: i64 = nx_hdl_clock(m)
202 if p_clk < 0 { return p_clk }
203 let p_rst: i64 = nx_hdl_reset(m)
204 if p_rst < 0 { return p_rst }
205 let p_rd_addr: i64 = nx_hdl_input(m, NX_CLINT_WIDTH_ADDR)
206 if p_rd_addr < 0 { return p_rd_addr }
207 let p_rd_en: i64 = nx_hdl_input(m, 1)
208 if p_rd_en < 0 { return p_rd_en }
209 let p_rd_data: i64 = nx_hdl_output(m, NX_CLINT_WIDTH_DATA)
210 if p_rd_data < 0 { return p_rd_data }
211 let p_rd_verdict: i64 = nx_hdl_output(m, NX_CLINT_WIDTH_VERDICT)
212 if p_rd_verdict < 0 { return p_rd_verdict }
213 let p_wr_addr: i64 = nx_hdl_input(m, NX_CLINT_WIDTH_ADDR)
214 if p_wr_addr < 0 { return p_wr_addr }
215 let p_wr_data: i64 = nx_hdl_input(m, NX_CLINT_WIDTH_DATA)
216 if p_wr_data < 0 { return p_wr_data }
217 let p_wr_en: i64 = nx_hdl_input(m, 1)
218 if p_wr_en < 0 { return p_wr_en }
219 let p_wr_verdict: i64 = nx_hdl_output(m, NX_CLINT_WIDTH_VERDICT)
220 if p_wr_verdict < 0 { return p_wr_verdict }
221 let p_mtip: i64 = nx_hdl_output(m, NX_CLINT_WIDTH_MTIP)
222 if p_mtip < 0 { return p_mtip }
223
224 ports.clk = p_clk
225 ports.reset = p_rst
226 ports.rd_addr = p_rd_addr
227 ports.rd_en = p_rd_en
228 ports.rd_data = p_rd_data
229 ports.rd_verdict = p_rd_verdict
230 ports.wr_addr = p_wr_addr
231 ports.wr_data = p_wr_data
232 ports.wr_en = p_wr_en
233 ports.wr_verdict = p_wr_verdict
234 ports.mtip_out = p_mtip
235
236 // Synth target: 2 64-bit flop registers (MTIME, MTIMECMP) + 64-bit
237 // unsigned comparator (~256 gates) + address decoder. Tier A
238 // FPGA fabric: trivial.
239 return NX_HDL_OK
240}