code wiki / _hdl_build / nx_driver_shapes.nx
nx_driver_shapes.nx source
↩ module page · 251 lines · 14992 B
1// SUPERSEDED 2026-06-13 (no-sprawl consolidation): the CANONICAL driver-from-spec path is the
2// op-list-driven nx_drv_proto_emit.nx (a new device class = a new op-list SPEC, zero emitter change).
3// This shape library + its consumer nx_drvgen are KEPT only as the byte-identical virtio re-emit
4// regression reference (NVMEGATE stage=B PROOF-1). See nx_drvgen.nx header + nvme_oplist_virt.spec.
5// nx_driver_shapes.nx -- the DEVICE-AGNOSTIC driver shape library (X-DRV-W1 stage A). This is
6// the emitter-of-emitters substrate for driver bring-up: a tiny rv64 mini-encoder plus four
7// SPEC-DATA-DRIVEN emit-op families that, between them, express the structure of ANY register-
8// programmed device-init driver. ZERO virtio (or NVMe) identity is baked here -- every offset
9// and constant an op touches arrives as a PARAMETER, pulled by the caller from a generic field
10// table. The device identity lives entirely in the spec; this library only knows how to turn
11// (offset, value) tuples into bytes. The four shapes:
12//
13// WIRE_TLV verify "reg-at-offset == const" / write "const -> reg-at-offset". The bring-up
14// handshake is a list of WIRE_TLV ops (virtio MagicValue/Version/DeviceID verifies
15// + Status ORs; NVMe would be CAP/VS verifies + AQA/ASQ/ACQ writes). Provided by
16// ds_emit_verify / ds_emit_verify_u / ds_emit_regwrite.
17// COMMAND_QUEUE bind a queue (write base/PFN regs) + kick a doorbell/notify + read an instrument
18// register back and verify it. virtio's QueuePFN+QueueNotify and NVMe's ASQ/ACQ+
19// SQyTDBL are the SAME op over different spec rows -- the caller threads the bind
20// regs, the kick reg/val, and the result reg/expected. The read-back verify is
21// ds_emit_verify / ds_emit_verify_u; the kick is ds_emit_regwrite.
22// STATE_MACHINE an enable/progress step that is EITHER monotonic-OR-write (virtio Status) OR
23// write-then-poll-until-bit-set (NVMe CC.EN -> CSTS.RDY). ds_emit_regwrite covers
24// the OR-write half; ds_emit_poll_until_set synthesises the poll-loop control flow
25// (a real backward branch) from a spec field -- the structure comes from DATA, not
26// from code (the eoe / X-AUT-006d principle). Virtio uses only the OR-write half;
27// the poll half is exercised by the 2nd device class (stage B).
28// STRUCT_WALK store struct fields at base_reg + memoff with width w, driven by a field table
29// (off, width, value). virtio's 16-byte split-virtqueue descriptor and NVMe's
30// 64-byte SQE are the SAME op over different field tables. Provided by
31// ds_emit_memstore (and ds_li32 / ds_li32u for materialising a struct base addr).
32//
33// Byte-reproducibility: every op keeps the two-pass measure/backfill invariant of the original
34// virtio emitter (instruction COUNT identical across passes; only immediates differ), so a forward
35// fail-branch target is structurally derived, never typed. This file is lifted VERBATIM (byte-for-
36// byte logic) from nx_virtio_hs_emit's encoder so the refactored virtio emitter that now CALLS
37// these ops re-emits its image byte-identically (the stage-A regression guard).
38// Sovereign: pure integer arithmetic, no syscalls beyond what the importer pulls in via
39// nx_syscalls. license_tier: ORIGINAL
40import "nx_syscalls.nx"
41
42// ---- generic target-platform map (qemu-virt rv64; hardware facts, not device identity) ----
43const DS_UART: i64 = 0x10000000 // NS16550A THR (write a byte = transmit a transcript char)
44const DS_FIN: i64 = 0x100000 // SiFive test finisher (write to exit the machine cleanly)
45const DS_PASS: i64 = 0x5555 // FINISHER_PASS low half -> clean halt
46// rv64 register numbers the shape ops use (a fixed calling convention, device-agnostic)
47const DS_X0: i64 = 0
48const DS_T0: i64 = 5 // UART base
49const DS_T1: i64 = 6 // scratch / transcript byte
50const DS_T2: i64 = 7 // device base
51const DS_T3: i64 = 28 // loaded register value (actual)
52const DS_T4: i64 = 29 // expected constant / poll mask
53const DS_T5: i64 = 30 // finisher base / struct-base scratch
54
55// ---- the rv64 mini-encoder (one form per function; integer-only, struct-free) ----
56func ds_lui(rd: i64, imm20: i64) -> i64 { return ((imm20 & 0xFFFFF) << 12) | (rd << 7) | 0x37 }
57func ds_addi(rd: i64, rs1: i64, imm: i64) -> i64 { return ((imm & 0xFFF) << 20) | (rs1 << 15) | (rd << 7) | 0x13 }
58// load; f3=2 -> LW (sign-extended 32-bit), f3=6 -> LWU (zero-extended). imm I-type (signed 12-bit).
59func ds_load(rd: i64, rs1: i64, f3: i64, imm: i64) -> i64 { return ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | 0x03 }
60// store; f3=0 -> SB, f3=1 -> SH, f3=2 -> SW. imm S-type split (signed 12-bit).
61func ds_store(rs2: i64, rs1: i64, f3: i64, imm: i64) -> i64 {
62 let hi: i64 = ((imm >> 5) & 0x7f) << 25
63 let lo: i64 = (imm & 0x1f) << 7
64 return hi | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | lo | 0x23
65}
66// B-type branch; f3=0 -> BEQ, f3=1 -> BNE. imm in bytes (signed, multiple of 2).
67func ds_branch(rs1: i64, rs2: i64, f3: i64, imm: i64) -> i64 {
68 let b12: i64 = ((imm >> 12) & 0x1) << 31
69 let b11: i64 = ((imm >> 11) & 0x1) << 7
70 let b10_5: i64 = ((imm >> 5) & 0x3f) << 25
71 let b4_1: i64 = ((imm >> 1) & 0xf) << 8
72 return b12 | b10_5 | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | b4_1 | b11 | 0x63
73}
74// J-type jal; imm in bytes (signed, multiple of 2).
75func ds_jal(rd: i64, imm: i64) -> i64 {
76 let b20: i64 = ((imm >> 20) & 0x1) << 31
77 let b19_12: i64 = ((imm >> 12) & 0xff) << 12
78 let b11: i64 = ((imm >> 11) & 0x1) << 20
79 let b10_1: i64 = ((imm >> 1) & 0x3ff) << 21
80 return b20 | b10_1 | b11 | b19_12 | (rd << 7) | 0x6f
81}
82// R-type AND (funct3=7, funct7=0): rd = rs1 & rs2.
83func ds_and(rd: i64, rs1: i64, rs2: i64) -> i64 { return (rs2 << 20) | (rs1 << 15) | (7 << 12) | (rd << 7) | 0x33 }
84// I-type shift-immediate (OP-IMM, opcode 0x13): f3=1 -> SLLI, f3=5 -> SRLI. rv64 shamt is 6 bits.
85func ds_shift(rd: i64, rs1: i64, f3: i64, shamt: i64) -> i64 { return ((shamt & 0x3f) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | 0x13 }
86
87func ds_w32(buf: *u8, off: i64, w: i64) -> i64 {
88 buf[off] = (w & 0xff) as u8
89 buf[off+1] = ((w >> 8) & 0xff) as u8
90 buf[off+2] = ((w >> 16) & 0xff) as u8
91 buf[off+3] = ((w >> 24) & 0xff) as u8
92 return off + 4
93}
94
95// load a full 32-bit constant into rd: lui hi + addi lo (sign-corrected). 2 words. On rv64 lui
96// sign-extends bit 31, so a bit-31-set value leaves rd = 0xFFFFFFFF_xxxxxxxx -- correct for MMIO
97// consts / feature masks (sign-extended compares) but WRONG for a true positive RAM address
98// (use ds_li32u for those).
99func ds_li32(buf: *u8, off: i64, rd: i64, val: i64) -> i64 {
100 var hi: i64 = (val >> 12) & 0xFFFFF
101 var lo: i64 = val & 0xFFF
102 if lo >= 0x800 { lo = lo - 0x1000; hi = (hi + 1) & 0xFFFFF }
103 var o: i64 = ds_w32(buf, off, ds_lui(rd, hi))
104 o = ds_w32(buf, o, ds_addi(rd, rd, lo))
105 return o
106}
107
108// load a ZERO-EXTENDED 32-bit constant (a true positive guest address) into rd. lui+addi then
109// slli rd,rd,32 ; srli rd,rd,32 clears the sign-extended upper 32 bits so a bit-31-set address
110// materialises positive. 4 words -- the canonical rv64 idiom for a 32-bit phys addr with bit 31 set.
111func ds_li32u(buf: *u8, off: i64, rd: i64, val: i64) -> i64 {
112 var o: i64 = ds_li32(buf, off, rd, val)
113 o = ds_w32(buf, o, ds_shift(rd, rd, 1, 32)) // slli rd,rd,32
114 o = ds_w32(buf, o, ds_shift(rd, rd, 5, 32)) // srli rd,rd,32
115 return o
116}
117
118// ---- transcript primitive: emit n bytes from s over the UART (t0 = UART base). 2 words/char. ----
119func ds_emit_str(buf: *u8, off: i64, s: *u8, n: i64) -> i64 {
120 var o: i64 = off
121 var i: i64 = 0
122 while i < n {
123 o = ds_w32(buf, o, ds_addi(DS_T1, DS_X0, s[i] as i64))
124 o = ds_w32(buf, o, ds_store(DS_T1, DS_T0, 0, 0)) // sb t1,0(t0)
125 i = i + 1
126 }
127 return o
128}
129
130// ==== SHAPE: WIRE_TLV ====
131// verify reg-at-offset == expected, else branch to fail_off (the finisher block).
132// lw t3, regoff(t2) ; li t4, expected ; bne t3,t4, (fail_off - pc). 4 words.
133func ds_emit_verify(buf: *u8, off: i64, regoff: i64, expected: i64, fail_off: i64) -> i64 {
134 var o: i64 = ds_w32(buf, off, ds_load(DS_T3, DS_T2, 2, regoff))
135 o = ds_li32(buf, o, DS_T4, expected)
136 let pc: i64 = o
137 o = ds_w32(buf, o, ds_branch(DS_T3, DS_T4, 1, fail_off - pc))
138 return o
139}
140
141// verify reg-at-offset == expected for an UNSIGNED 32-bit value (e.g. a guest phys addr whose bit
142// 31 is set), else branch to fail_off. Uses LWU (f3=6, zero-extend) so the loaded value matches
143// the lui-built positive expected. 4 words (same shape as ds_emit_verify, only the load f3 differs
144// -> two-pass byte-reproducibility preserved).
145func ds_emit_verify_u(buf: *u8, off: i64, regoff: i64, expected: i64, fail_off: i64) -> i64 {
146 var o: i64 = ds_w32(buf, off, ds_load(DS_T3, DS_T2, 6, regoff))
147 o = ds_li32(buf, o, DS_T4, expected)
148 let pc: i64 = o
149 o = ds_w32(buf, o, ds_branch(DS_T3, DS_T4, 1, fail_off - pc))
150 return o
151}
152
153// write a value into a MMIO register (a Status latch ORs writes; a config reg latches the value).
154// li t1,val ; sw t1, regoff(t2). 3 words.
155func ds_emit_regwrite(buf: *u8, off: i64, regoff: i64, val: i64) -> i64 {
156 var o: i64 = ds_li32(buf, off, DS_T1, val)
157 o = ds_w32(buf, o, ds_store(DS_T1, DS_T2, 2, regoff))
158 return o
159}
160
161// confirm (reg & mask) != 0, else branch to fail_off (a WIRE_TLV "bit stuck" check).
162// lw t3, regoff(t2) ; li t4, mask ; and t3,t3,t4 ; beq t3,x0, fail. 5 words.
163func ds_emit_checkbit(buf: *u8, off: i64, regoff: i64, mask: i64, fail_off: i64) -> i64 {
164 var o: i64 = ds_w32(buf, off, ds_load(DS_T3, DS_T2, 2, regoff))
165 o = ds_li32(buf, o, DS_T4, mask)
166 o = ds_w32(buf, o, ds_and(DS_T3, DS_T3, DS_T4))
167 let pc: i64 = o
168 o = ds_w32(buf, o, ds_branch(DS_T3, DS_X0, 0, fail_off - pc))
169 return o
170}
171
172// write a value into a MMIO register at an ABSOLUTE address (not a 12-bit offset from the
173// device base). Needed when a register lives FAR from the base (NVMe's doorbell page at
174// base+0x1000 and the CqPeek instrument at base+0x1008 are beyond the rv64 sw/lw signed
175// 12-bit immediate range of +/-2047). Materialise the full address into a scratch register
176// (DS_T5) and store at offset 0. li t5,regaddr ; li t1,val ; sw t1,0(t5). 4 words. Generic:
177// the caller threads regaddr = base + regoff (the device identity is data, not code).
178func ds_emit_regwrite_far(buf: *u8, off: i64, regaddr: i64, val: i64) -> i64 {
179 var o: i64 = ds_li32(buf, off, DS_T5, regaddr)
180 o = ds_li32(buf, o, DS_T1, val)
181 o = ds_w32(buf, o, ds_store(DS_T1, DS_T5, 2, 0))
182 return o
183}
184
185// verify reg-at-ABSOLUTE-address == expected (unsigned, LWU), else branch to fail_off. The FAR
186// twin of ds_emit_verify_u for a register beyond the 12-bit immediate range (NVMe's CqPeek at
187// base+0x1008). li t5,regaddr ; lwu t3,0(t5) ; li t4,expected ; bne t3,t4, fail. 6 words.
188func ds_emit_verify_far(buf: *u8, off: i64, regaddr: i64, expected: i64, fail_off: i64) -> i64 {
189 var o: i64 = ds_li32(buf, off, DS_T5, regaddr)
190 o = ds_w32(buf, o, ds_load(DS_T3, DS_T5, 6, 0))
191 o = ds_li32(buf, o, DS_T4, expected)
192 let pc: i64 = o
193 o = ds_w32(buf, o, ds_branch(DS_T3, DS_T4, 1, fail_off - pc))
194 return o
195}
196
197// ==== SHAPE: STRUCT_WALK ====
198// store a 32-bit immediate into RAM at base_reg + memoff (f3=2 -> SW, f3=1 -> SH, f3=0 -> SB).
199// li t1,val ; sw/sh/sb t1, memoff(base_reg). 3 words. (base_reg already holds the struct addr.)
200func ds_emit_memstore(buf: *u8, off: i64, base_reg: i64, memoff: i64, f3: i64, val: i64) -> i64 {
201 var o: i64 = ds_li32(buf, off, DS_T1, val)
202 o = ds_w32(buf, o, ds_store(DS_T1, base_reg, f3, memoff))
203 return o
204}
205
206// ==== SHAPE: STATE_MACHINE (poll half) ====
207// write-then-poll-until-bit-set: the device-enable progress step that virtio does NOT have but
208// NVMe (CC.EN -> CSTS.RDY) does. Emits a REAL backward branch (a poll loop): the read of the
209// status reg, the AND with the ready mask, and a BEQ back to the read while the bit is clear.
210// li t1, enable_val ; sw t1, en_off(t2) -- write the enable
211// L: lw t3, rdy_off(t2) ; li t4, rdy_mask ; and t3,t3,t4 ; beq t3,x0, L (backward branch). 8 words.
212// The backward branch (negative B-type immediate) is the structural fingerprint that distinguishes
213// a polled state machine from virtio's straight-line forward-only handshake -- constant-lifting a
214// forward-only image provably cannot introduce a loop (the eoe / X-AUT-006d argument).
215func ds_emit_poll_until_set(buf: *u8, off: i64, en_off: i64, en_val: i64, rdy_off: i64, rdy_mask: i64) -> i64 {
216 var o: i64 = ds_li32(buf, off, DS_T1, en_val)
217 o = ds_w32(buf, o, ds_store(DS_T1, DS_T2, 2, en_off)) // sw enable -> en reg
218 let loop_pc: i64 = o // L:
219 o = ds_w32(buf, o, ds_load(DS_T3, DS_T2, 2, rdy_off)) // lw t3, rdy(t2)
220 o = ds_li32(buf, o, DS_T4, rdy_mask) // li t4, mask
221 o = ds_w32(buf, o, ds_and(DS_T3, DS_T3, DS_T4)) // and t3,t3,t4
222 let pc: i64 = o
223 o = ds_w32(buf, o, ds_branch(DS_T3, DS_X0, 0, loop_pc - pc)) // beq t3,x0, L (backward: loop_pc - pc < 0)
224 return o
225}
226
227// poll-until-bit-set over a MEMORY location pointed at by base_reg (NOT a device register).
228// virtio has no such loop; NVMe's CQE phase-tag poll (spin on the completion entry in guest RAM
229// until the device sets the phase bit) does. No enable-write half -- the device-side post already
230// happened (the doorbell DMA ran on the kick); this is purely the driver SPINNING on the result.
231// L: lw t3, poll_off(base_reg) ; li t4, mask ; and t3,t3,t4 ; beq t3,x0, L (backward branch). 5 words.
232// Same STATE_MACHINE family as ds_emit_poll_until_set -- a real backward B-type immediate, the
233// structural fingerprint a forward-only image (virtio) provably cannot carry (eoe / X-AUT-006d).
234func ds_emit_poll_mem_until_set(buf: *u8, off: i64, base_reg: i64, poll_off: i64, mask: i64) -> i64 {
235 let loop_pc: i64 = off // L:
236 var o: i64 = ds_w32(buf, off, ds_load(DS_T3, base_reg, 2, poll_off)) // lw t3, poll(base_reg)
237 o = ds_li32(buf, o, DS_T4, mask) // li t4, mask
238 o = ds_w32(buf, o, ds_and(DS_T3, DS_T3, DS_T4)) // and t3,t3,t4
239 let pc: i64 = o
240 o = ds_w32(buf, o, ds_branch(DS_T3, DS_X0, 0, loop_pc - pc)) // beq t3,x0, L (backward)
241 return o
242}
243
244// ==== finisher: li t5,FIN ; li t1,PASS ; sw t1,0(t5) -> clean halt ; jal x0,0 spin. 6 words. ====
245func ds_emit_finisher(buf: *u8, off: i64) -> i64 {
246 var o: i64 = ds_li32(buf, off, DS_T5, DS_FIN)
247 o = ds_li32(buf, o, DS_T1, DS_PASS)
248 o = ds_w32(buf, o, ds_store(DS_T1, DS_T5, 2, 0))
249 o = ds_w32(buf, o, ds_jal(DS_X0, 0))
250 return o
251}