code wiki / _hdl_build / rv64im_min_nvme.nx

rv64im_min_nvme.nx source

↩ module page · 332 lines · 16304 B

1// rv64im_min_nvme.nx -- a SOVEREIGN NVMe-class PCI/MMIO controller device model (X-DRV-W1 2// stage B, the 2nd device class). This is the TWIN of rv64im_min_virtio.nx but for a 3// genuinely DIFFERENT bring-up protocol: where virtio is a straight-line OR-write status 4// ladder, NVMe is a write-then-POLL enable handshake (CC.EN -> poll CSTS.RDY) plus an 5// admin Submission/Completion queue pair bound by 64-bit BASE-ADDRESS registers (ASQ/ACQ, 6// NOT a single PFN<<12), a 64-byte SQE the driver lays in guest RAM, ring advance by 7// TAIL/HEAD DOORBELL writes, and a 16-byte CQE with a PHASE-TAG bit the driver polls. 8// 9// The device models ONE controller window [0x10003000, 0x10003000+0x2000). The KEY 10// behaviour: on a SQ-Tail-doorbell write (off_sq0tdbl) the device runs the doorbell DMA -- 11// it fetches the 64-byte SQE from ASQ_base (STRUCT_WALK over guest RAM: reads opcode + CID), 12// executes a trivial admin command, POSTS a 16-byte CQE into ACQ_base (writes SQHD/CID/status 13// with the phase bit set), and latches a NvmeCqPeek RO word = (CID<<16)|status|phase. CSTS.RDY 14// is a latched flop set when CC.EN was written 1 (cleared on CC.EN=0 reset), so the driver's 15// poll-until-RDY loop terminates. The virtio + net device models are untouched (additive, 16// separate instance) so PROOF-1's SECTGATE stays GREEN. 17// 18// NVMe/PCI here is a last-mile interop PROBE + a measuring stick to EXCEED -- the emitted 19// driver and the shapes are Nishi-native (the device identity lives ENTIRELY in the spec; the 20// shape library that emits the driver knows zero NVMe). 21// Status: SEED. 2026-06-13. Enable handshake + admin SQ/CQ doorbell DMA + phase-tagged CQE. 22// license_tier: ORIGINAL 23 24import "nx_syscalls.nx" 25import "nishi_hdl_primitives.nx" 26 27// ===== MMIO addresses ================================================= 28// One NVMe controller register window, separate from the virtio windows (0x10001000 blk, 29// 0x10002000 net). A 0x2000-byte window so the doorbell page (off 0x1000..) fits. 30const NX_NVME_BASE: i64 = 0x10003000 31const NX_NVME_END: i64 = 0x10005000 32 33// ===== NVMe controller register offsets (NVMe 1.4 base spec, the bring-up subset) ===== 34// 0x00 CAP (RO) controller capabilities (DSTRD/MQES const -- the device offers them) 35// 0x08 VS (RO) version (0x00010400 = 1.4.0) 36// 0x14 CC (RW) controller config; EN = bit0 (the enable the driver writes) 37// 0x1C CSTS (RO) controller status; RDY = bit0 (latched when CC.EN written 1) 38// 0x24 AQA (RW) admin queue attributes (ASQS/ACQS sizes -- latched, no semantics yet) 39// 0x28 ASQ_LO / 0x2C ASQ_HI (RW) admin SQ base address (64-bit, split lo/hi) 40// 0x30 ACQ_LO / 0x34 ACQ_HI (RW) admin CQ base address (64-bit, split lo/hi) 41// 0x1000 SQ0TDBL (WO) admin SQ tail doorbell (the kick that runs the doorbell DMA) 42// 0x1004 CQ0HDBL (WO) admin CQ head doorbell (the driver's completion ack) 43// 0x1008 CQPEEK (RO) sovereign-only instrument = (CID<<16)|status|phase, the binding-proof 44// register mirroring virtio's QueueSectPeek -- the driver reads it back. 45const NX_NVME_OFF_CAP: i64 = 0x00 46const NX_NVME_OFF_VS: i64 = 0x08 47const NX_NVME_OFF_CC: i64 = 0x14 48const NX_NVME_OFF_CSTS: i64 = 0x1C 49const NX_NVME_OFF_AQA: i64 = 0x24 50const NX_NVME_OFF_ASQ_LO: i64 = 0x28 51const NX_NVME_OFF_ASQ_HI: i64 = 0x2C 52const NX_NVME_OFF_ACQ_LO: i64 = 0x30 53const NX_NVME_OFF_ACQ_HI: i64 = 0x34 54const NX_NVME_OFF_SQ0TDBL: i64 = 0x1000 55const NX_NVME_OFF_CQ0HDBL: i64 = 0x1004 56const NX_NVME_OFF_CQPEEK: i64 = 0x1008 57// CQPEEK read-alias at a SMALL offset so the GENERIC op-list emitter (nx_drv_proto_emit) can read 58// it back with a 12-bit load immediate (op verifyu base+0x40); the real 0x1008 register is kept for 59// the bespoke nx_drvgen path. Additive RO alias -- same latched (CID<<16)|status|phase word. 60const NX_NVME_OFF_CQPEEK_LO: i64 = 0x40 61 62// ===== Device identity (RO register values) ================================================= 63const NX_NVME_CAP: i64 = 0x0000000000200001 // MQES=32 (0x1F+1), DSTRD=0 (low bits); RO const 64const NX_NVME_VS: i64 = 0x00010400 // NVMe 1.4.0 65const NX_NVME_CC_EN: i64 = 1 // CC.EN = bit0 66const NX_NVME_CSTS_RDY: i64 = 1 // CSTS.RDY = bit0 67 68// ===== SQE / CQE layout (64-byte SQE, 16-byte CQE) ================================================= 69// The driver lays a 64-byte Submission Queue Entry at ASQ_base. The device reads: 70// sqe.opcode @ byte 0 (Identify=0x06 / a no-op admin cmd) 71// sqe.cid @ byte 2 (16-bit command identifier the device ECHOES back into the CQE) 72// On completion the device posts a 16-byte Completion Queue Entry at ACQ_base: 73// cqe.cid @ byte 12 (16-bit, echoed from the SQE) 74// cqe.status @ byte 14 (16-bit; bit0 = phase tag, bits 1.. = status code (0=success)) 75const NX_NVME_SQE_OFF_OPCODE: i64 = 0 76const NX_NVME_SQE_OFF_CID: i64 = 2 77const NX_NVME_CQE_OFF_CID: i64 = 12 78const NX_NVME_CQE_OFF_STATUS: i64 = 14 79const NX_NVME_PHASE_BIT: i64 = 1 // CQE status bit0 -- the driver polls it 80 81// ===== Verdicts ================================================= 82const NX_NVME_OK: i64 = 0 83const NX_NVME_ADDR_OUT_OF_RANGE: i64 = 1 84 85// ===== Storage (caller allocates an NX_NVME_SLOT_N-i64 backing buffer) ===== 86// slot 0: CC (latched controller-config value the driver wrote) 87// slot 1: CSTS (latched controller status; RDY flop = bit0) 88// slot 2: AQA (latched admin queue attributes) 89// slot 3: ASQ_LO (latched admin SQ base low word) 90// slot 4: ASQ_HI (latched admin SQ base high word) 91// slot 5: ACQ_LO (latched admin CQ base low word) 92// slot 6: ACQ_HI (latched admin CQ base high word) 93// slot 7: SQTAIL (count of SQ tail doorbell rings) 94// slot 8: CQHEAD (internal CQ head; advanced on a CQ head doorbell ack) 95// slot 9: CQPEEK ((CID<<16)|status|phase latched after the device posts the CQE; RO) 96const NX_NVME_SLOT_CC: i64 = 0 97const NX_NVME_SLOT_CSTS: i64 = 1 98const NX_NVME_SLOT_AQA: i64 = 2 99const NX_NVME_SLOT_ASQ_LO: i64 = 3 100const NX_NVME_SLOT_ASQ_HI: i64 = 4 101const NX_NVME_SLOT_ACQ_LO: i64 = 5 102const NX_NVME_SLOT_ACQ_HI: i64 = 6 103const NX_NVME_SLOT_SQTAIL: i64 = 7 104const NX_NVME_SLOT_CQHEAD: i64 = 8 105const NX_NVME_SLOT_CQPEEK: i64 = 9 106const NX_NVME_SLOT_N: i64 = 10 107 108struct NxNvmeCtrl { 109 storage: *i64 // NX_NVME_SLOT_N i64s 110 valid: i64 111 base: i64 // this instance's MMIO base (0x10003000) 112} 113 114// shared storage reset (controller reset: CC=0, CSTS.RDY clear, queues forgotten). 115func nx_nvme_reset_storage(storage: *i64) -> i64 { 116 storage[NX_NVME_SLOT_CC] = 0 117 storage[NX_NVME_SLOT_CSTS] = 0 118 storage[NX_NVME_SLOT_AQA] = 0 119 storage[NX_NVME_SLOT_ASQ_LO] = 0 120 storage[NX_NVME_SLOT_ASQ_HI] = 0 121 storage[NX_NVME_SLOT_ACQ_LO] = 0 122 storage[NX_NVME_SLOT_ACQ_HI] = 0 123 storage[NX_NVME_SLOT_SQTAIL] = 0 124 storage[NX_NVME_SLOT_CQHEAD] = 0 125 storage[NX_NVME_SLOT_CQPEEK] = 0 126 return NX_NVME_OK 127} 128 129func nx_nvme_init(c: *NxNvmeCtrl, storage: *i64) -> i64 { 130 if (c as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 131 if (storage as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 132 c.storage = storage 133 c.valid = 1 134 c.base = NX_NVME_BASE 135 nx_nvme_reset_storage(storage) 136 return NX_NVME_OK 137} 138 139// ===== Range check ================================================= 140func nx_nvme_addr_in_range(c: *NxNvmeCtrl, addr: i64) -> i64 { 141 if addr < c.base { return 0 } 142 if addr >= c.base + 0x2000 { return 0 } 143 return 1 144} 145 146// ===== little-endian DMA helpers over the sim's flat guest RAM (DRY: same bounds-checked 147// device/memory-boundary contract as the virtio model) ==================================== 148func nx_nvme_dma_rd16(mem_buf: *u8, off: i64) -> i64 { 149 let b0: i64 = mem_buf[off] as i64 150 let b1: i64 = mem_buf[off + 1] as i64 151 return b0 | (b1 << 8) 152} 153func nx_nvme_dma_rd32(mem_buf: *u8, off: i64) -> i64 { 154 let b0: i64 = mem_buf[off] as i64 155 let b1: i64 = mem_buf[off + 1] as i64 156 let b2: i64 = mem_buf[off + 2] as i64 157 let b3: i64 = mem_buf[off + 3] as i64 158 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) 159} 160func nx_nvme_dma_wr16(mem_buf: *u8, off: i64, val: i64) -> i64 { 161 mem_buf[off] = (val & 0xff) as u8 162 mem_buf[off + 1] = ((val >> 8) & 0xff) as u8 163 return 0 164} 165func nx_nvme_dma_wr32(mem_buf: *u8, off: i64, val: i64) -> i64 { 166 mem_buf[off] = (val & 0xff) as u8 167 mem_buf[off + 1] = ((val >> 8) & 0xff) as u8 168 mem_buf[off + 2] = ((val >> 16) & 0xff) as u8 169 mem_buf[off + 3] = ((val >> 24) & 0xff) as u8 170 return 0 171} 172func nx_nvme_dma_inrange(off: i64, w: i64, mem_size: i64) -> i64 { 173 if off < 0 { return 0 } 174 if off > mem_size - w { return 0 } 175 return 1 176} 177 178// ===== MMIO read (32-bit) ================================================= 179// CAP/VS return their RO spec constants; CSTS returns the latched status (RDY flop); 180// CC/AQA/ASQ/ACQ read back what was latched; CQPEEK returns the latched completion proof 181// word; everything else returns 0 (sparse). value_out[0] gets the word. 182func nx_nvme_read32(c: *NxNvmeCtrl, addr: i64, value_out: *i64) -> i64 { 183 if c.valid != 1 { return 0 - NX_NVME_ADDR_OUT_OF_RANGE } 184 if (value_out as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 185 if nx_nvme_addr_in_range(c, addr) != 1 { return 0 - NX_NVME_ADDR_OUT_OF_RANGE } 186 let off: i64 = addr - c.base 187 if off == NX_NVME_OFF_CAP { 188 value_out[0] = NX_NVME_CAP & 0xffffffff 189 return NX_NVME_OK 190 } 191 if off == NX_NVME_OFF_VS { 192 value_out[0] = NX_NVME_VS 193 return NX_NVME_OK 194 } 195 if off == NX_NVME_OFF_CC { 196 value_out[0] = c.storage[NX_NVME_SLOT_CC] 197 return NX_NVME_OK 198 } 199 if off == NX_NVME_OFF_CSTS { 200 // RDY mirrors a latched flop set when CC.EN was written 1. 201 value_out[0] = c.storage[NX_NVME_SLOT_CSTS] 202 return NX_NVME_OK 203 } 204 if off == NX_NVME_OFF_AQA { 205 value_out[0] = c.storage[NX_NVME_SLOT_AQA] 206 return NX_NVME_OK 207 } 208 if off == NX_NVME_OFF_ASQ_LO { 209 value_out[0] = c.storage[NX_NVME_SLOT_ASQ_LO] 210 return NX_NVME_OK 211 } 212 if off == NX_NVME_OFF_ASQ_HI { 213 value_out[0] = c.storage[NX_NVME_SLOT_ASQ_HI] 214 return NX_NVME_OK 215 } 216 if off == NX_NVME_OFF_ACQ_LO { 217 value_out[0] = c.storage[NX_NVME_SLOT_ACQ_LO] 218 return NX_NVME_OK 219 } 220 if off == NX_NVME_OFF_ACQ_HI { 221 value_out[0] = c.storage[NX_NVME_SLOT_ACQ_HI] 222 return NX_NVME_OK 223 } 224 if off == NX_NVME_OFF_CQPEEK { 225 // RO sovereign-only instrument: (CID<<16)|status|phase the device latched after 226 // posting the CQE. The driver reads it back and verifies == cqpeek_expected. 227 value_out[0] = c.storage[NX_NVME_SLOT_CQPEEK] & 0xffffffff 228 return NX_NVME_OK 229 } 230 if off == NX_NVME_OFF_CQPEEK_LO { 231 // small-offset RO alias of CQPEEK (for the generic op-list driver's 12-bit-immediate read). 232 value_out[0] = c.storage[NX_NVME_SLOT_CQPEEK] & 0xffffffff 233 return NX_NVME_OK 234 } 235 value_out[0] = 0 236 return NX_NVME_OK 237} 238 239// ===== Admin SQ-Tail doorbell DMA (the heart of the 2nd-class round-trip) ===================== 240// On a SQ-Tail-doorbell write the device: 241// 1) fetches the 64-byte SQE from ASQ_base (STRUCT_WALK: reads opcode @+0, CID @+2), 242// 2) executes a trivial admin command (a no-op / Identify-class command), 243// 3) POSTS a 16-byte CQE into ACQ_base (writes CID @+12, status @+14 with phase bit set), 244// 4) latches NvmeCqPeek = (CID<<16) | status | phase. 245// ASQ/ACQ base addresses are guest-physical; their byte offset into mem_buf = phys - mem_base. 246// Bounds-checked at the device/memory boundary (same contract as virtio): an out-of-range 247// queue base makes the DMA a no-op (CQPEEK stays 0) -- no out-of-bounds RAM access. 248func nx_nvme_doorbell_dma(c: *NxNvmeCtrl, mem_buf: *u8, mem_base: i64, mem_size: i64) -> i64 { 249 if c.valid != 1 { return 0 - NX_NVME_ADDR_OUT_OF_RANGE } 250 if (mem_buf as i64) == 0 { return 0 - NX_NVME_ADDR_OUT_OF_RANGE } 251 c.storage[NX_NVME_SLOT_SQTAIL] = c.storage[NX_NVME_SLOT_SQTAIL] + 1 252 // the device only services the doorbell once the controller is enabled + ready. 253 if (c.storage[NX_NVME_SLOT_CSTS] & NX_NVME_CSTS_RDY) == 0 { return NX_NVME_OK } 254 // ---- 1) fetch the 64-byte SQE from ASQ_base (low 32 bits give the in-RAM offset) ---- 255 let asq_phys: i64 = c.storage[NX_NVME_SLOT_ASQ_LO] 256 if asq_phys < mem_base { return 0 - NX_NVME_ADDR_OUT_OF_RANGE } 257 let asq_off: i64 = asq_phys - mem_base 258 if nx_nvme_dma_inrange(asq_off + NX_NVME_SQE_OFF_CID, 2, mem_size) != 1 { return 0 - NX_NVME_ADDR_OUT_OF_RANGE } 259 let opcode: i64 = mem_buf[asq_off + NX_NVME_SQE_OFF_OPCODE] as i64 260 let cid: i64 = nx_nvme_dma_rd16(mem_buf, asq_off + NX_NVME_SQE_OFF_CID) 261 // ---- 2) execute the admin command (trivial): status = success (0) on any valid opcode. 262 var status_code: i64 = 0 263 if opcode == 0 { status_code = 0 } // opcode read but not gated; structural exercise 264 // ---- 3) POST the 16-byte CQE into ACQ_base: cid @+12, status @+14 (bit0 = phase) ---- 265 let acq_phys: i64 = c.storage[NX_NVME_SLOT_ACQ_LO] 266 if acq_phys < mem_base { return 0 - NX_NVME_ADDR_OUT_OF_RANGE } 267 let acq_off: i64 = acq_phys - mem_base 268 if nx_nvme_dma_inrange(acq_off + NX_NVME_CQE_OFF_STATUS, 2, mem_size) != 1 { return 0 - NX_NVME_ADDR_OUT_OF_RANGE } 269 let status_field: i64 = ((status_code & 0x7fff) << 1) | NX_NVME_PHASE_BIT 270 nx_nvme_dma_wr16(mem_buf, acq_off + NX_NVME_CQE_OFF_CID, cid & 0xffff) 271 nx_nvme_dma_wr16(mem_buf, acq_off + NX_NVME_CQE_OFF_STATUS, status_field & 0xffff) 272 // ---- 4) latch NvmeCqPeek = (CID<<16) | status | phase (the binding-proof word) ---- 273 let peek: i64 = ((cid & 0xffff) << 16) | (status_field & 0xffff) 274 c.storage[NX_NVME_SLOT_CQPEEK] = peek & 0xffffffff 275 return NX_NVME_OK 276} 277 278// ===== MMIO write (32-bit) ================================================= 279// CC latches the config; writing CC.EN=1 sets the CSTS.RDY flop (enable), CC.EN=0 clears it 280// (controller reset, queues forgotten). AQA/ASQ/ACQ base regs latch their value. The SQ-Tail 281// doorbell runs the doorbell DMA (via the sim's store32 dispatch, which hands in guest RAM). 282// The CQ-Head doorbell acks (advances the internal CQ head). RO regs ignore writes. 283func nx_nvme_write32(c: *NxNvmeCtrl, addr: i64, value: i64) -> i64 { 284 if c.valid != 1 { return 0 - NX_NVME_ADDR_OUT_OF_RANGE } 285 if nx_nvme_addr_in_range(c, addr) != 1 { return 0 - NX_NVME_ADDR_OUT_OF_RANGE } 286 let off: i64 = addr - c.base 287 if off == NX_NVME_OFF_CC { 288 c.storage[NX_NVME_SLOT_CC] = value & 0xffffffff 289 if (value & NX_NVME_CC_EN) != 0 { 290 // CC.EN=1 -> set CSTS.RDY (the latched ready flop the driver polls for). 291 c.storage[NX_NVME_SLOT_CSTS] = c.storage[NX_NVME_SLOT_CSTS] | NX_NVME_CSTS_RDY 292 } else { 293 // CC.EN=0 -> controller reset: clear RDY + forget queues. 294 nx_nvme_reset_storage(c.storage) 295 } 296 return NX_NVME_OK 297 } 298 if off == NX_NVME_OFF_AQA { 299 c.storage[NX_NVME_SLOT_AQA] = value & 0xffffffff 300 return NX_NVME_OK 301 } 302 if off == NX_NVME_OFF_ASQ_LO { 303 c.storage[NX_NVME_SLOT_ASQ_LO] = value & 0xffffffff 304 return NX_NVME_OK 305 } 306 if off == NX_NVME_OFF_ASQ_HI { 307 c.storage[NX_NVME_SLOT_ASQ_HI] = value & 0xffffffff 308 return NX_NVME_OK 309 } 310 if off == NX_NVME_OFF_ACQ_LO { 311 c.storage[NX_NVME_SLOT_ACQ_LO] = value & 0xffffffff 312 return NX_NVME_OK 313 } 314 if off == NX_NVME_OFF_ACQ_HI { 315 c.storage[NX_NVME_SLOT_ACQ_HI] = value & 0xffffffff 316 return NX_NVME_OK 317 } 318 if off == NX_NVME_OFF_CQ0HDBL { 319 // CQ head doorbell: ack -- advance the internal CQ head. 320 c.storage[NX_NVME_SLOT_CQHEAD] = c.storage[NX_NVME_SLOT_CQHEAD] + 1 321 return NX_NVME_OK 322 } 323 // CAP/VS/CSTS/CQPEEK are read-only: writes silently dropped. SQ0TDBL is handled by the 324 // sim's store32 dispatch (it has mem_buf in hand) -- nx_nvme_doorbell_dma is called there. 325 return NX_NVME_OK 326} 327 328// ===== Status accessor (harness helper) ================================================= 329func nx_nvme_csts(c: *NxNvmeCtrl) -> i64 { 330 if c.valid != 1 { return 0 } 331 return c.storage[NX_NVME_SLOT_CSTS] 332}