code wiki / _hdl_build / rv64im_min_decoder.nx

rv64im_min_decoder.nx source

↩ module page · 291 lines · 11360 B

1// rv64im_min_decoder.nx -- first concrete use of the NishiHDL DSL. 2// 3// Decodes RV64I + M instructions into a sealed-enum opcode kind + 4// operand-extracted fields (rd, rs1, rs2, immediate). This is the 5// fetch-decode stage of the Tier A FPGA target (see 6// hdl/rv64im_min_target_spec.md for the silicon contract). 7// 8// Status: SEED. 2026-05-26. Decoder logic only; execute / mem / 9// writeback land in follow-on commits. 10// 11// Why decoder first: 12// 1. Smallest standalone module that exercises every NishiHDL 13// primitive (wires, modules, connect, sealed enums). 14// 2. Verifiable against ANY known RV64IM-binary -- correctness 15// can be ground-truthed before any execute logic exists. 16// 3. Decoder shape is identical across single-cycle, 5-stage, 17// and out-of-order pipelines; so this module survives any 18// future pipeline-style change. 19// 20// RV64I instruction format (per riscv-spec): 21// bits 31..25 funct7 22// bits 24..20 rs2 23// bits 19..15 rs1 24// bits 14..12 funct3 25// bits 11..7 rd 26// bits 6..0 opcode 27// 28// The seven base RV64I opcodes (low 7 bits): 29// 0110011 R-type add/sub/sll/slt/sltu/xor/srl/sra/or/and (also M ext) 30// 0010011 I-type addi/slti/sltiu/xori/ori/andi/slli/srli/srai 31// 0000011 Load lb/lh/lw/ld/lbu/lhu/lwu 32// 0100011 Store sb/sh/sw/sd 33// 1100011 Branch beq/bne/blt/bge/bltu/bgeu 34// 1101111 jal pc-relative jump-and-link 35// 1100111 jalr reg-indirect jump-and-link 36// 0110111 lui load upper immediate 37// 0010111 auipc add upper immediate to pc 38// 1110011 System ecall/ebreak/csrrw/csrrs/csrrc/csrrwi/csrrsi/csrrci 39// 0001111 Fence fence/fence.i 40// 41// RV64I-64 widening adds W-suffixed ops: 42// 0111011 R-type 32-bit addw/subw/sllw/srlw/sraw + M-ext mulw/divw/... 43// 0011011 I-type 32-bit addiw/slliw/srliw/sraiw 44// 45// M extension overloads opcode 0110011 with funct7=0000001: 46// mul / mulh / mulhsu / mulhu / div / divu / rem / remu 47// And opcode 0111011 with funct7=0000001: 48// mulw / divw / divuw / remw / remuw 49 50import "nx_syscalls.nx" 51import "nishi_hdl_primitives.nx" 52 53// ===== Decoded opcode kinds (sealed enum) ================================================= 54// 55// Every RV64IM instruction collapses to one of these kinds. Sealed: 56// decoders that produce a kind >= NX_RV64IM_OP_N are bugs. 57 58const NX_RV64IM_OP_INVALID: i64 = 0 // illegal instruction (raises exc) 59 60// RV64I base 61const NX_RV64IM_OP_LUI: i64 = 1 62const NX_RV64IM_OP_AUIPC: i64 = 2 63const NX_RV64IM_OP_JAL: i64 = 3 64const NX_RV64IM_OP_JALR: i64 = 4 65const NX_RV64IM_OP_BRANCH: i64 = 5 // beq/bne/blt/bge/bltu/bgeu (funct3 disambiguates) 66const NX_RV64IM_OP_LOAD: i64 = 6 // lb/lh/lw/ld/lbu/lhu/lwu 67const NX_RV64IM_OP_STORE: i64 = 7 // sb/sh/sw/sd 68const NX_RV64IM_OP_OP_IMM: i64 = 8 // addi/slti/sltiu/xori/ori/andi/slli/srli/srai 69const NX_RV64IM_OP_OP: i64 = 9 // add/sub/sll/slt/sltu/xor/srl/sra/or/and 70const NX_RV64IM_OP_OP_IMM_32: i64 = 10 // addiw/slliw/srliw/sraiw 71const NX_RV64IM_OP_OP_32: i64 = 11 // addw/subw/sllw/srlw/sraw 72const NX_RV64IM_OP_FENCE: i64 = 12 73const NX_RV64IM_OP_SYSTEM: i64 = 13 // ecall/ebreak/csrrw/... 74 75// M extension (overlays OP / OP_32 when funct7=0000001) 76const NX_RV64IM_OP_M_MUL: i64 = 14 // mul/mulh/mulhsu/mulhu 77const NX_RV64IM_OP_M_DIV: i64 = 15 // div/divu/rem/remu 78const NX_RV64IM_OP_M_MUL_32: i64 = 16 // mulw 79const NX_RV64IM_OP_M_DIV_32: i64 = 17 // divw/divuw/remw/remuw 80 81// A extension (atomics): LR/SC + AMO* share opcode 0x2f; funct5 (inst[31:27]) disambiguates, 82// funct3 selects width (2 = .w, 3 = .d). Added 2026-07-18 to close the RV64A gap (F107h). 83const NX_RV64IM_OP_AMO: i64 = 18 84 85const NX_RV64IM_OP_N: i64 = 19 86 87func nx_rv64im_op_is_valid(op: i64) -> i64 { 88 if op < 0 { return 0 } 89 if op >= NX_RV64IM_OP_N { return 0 } 90 return 1 91} 92 93// ===== Bit-field extraction helpers ================================================= 94// 95// These mirror the riscv-spec slice notation. Inputs are i64 96// (a 32-bit instruction fits comfortably; high bits are zero). 97 98func nx_rv64im_opcode(inst: i64) -> i64 { 99 return inst & 0x7f 100} 101 102func nx_rv64im_rd(inst: i64) -> i64 { 103 return (inst >> 7) & 0x1f 104} 105 106func nx_rv64im_funct3(inst: i64) -> i64 { 107 return (inst >> 12) & 0x7 108} 109 110func nx_rv64im_rs1(inst: i64) -> i64 { 111 return (inst >> 15) & 0x1f 112} 113 114func nx_rv64im_rs2(inst: i64) -> i64 { 115 return (inst >> 20) & 0x1f 116} 117 118func nx_rv64im_funct7(inst: i64) -> i64 { 119 return (inst >> 25) & 0x7f 120} 121 122// ===== Immediate decoders (per format) ================================================= 123// 124// Each instruction format encodes its immediate differently; the 125// decoder produces sign-extended 64-bit immediates ready for the 126// execute stage. Sign extension uses arithmetic shift via the 127// upper-bit-mask trick (NishiLang i64 shift is logical). 128 129func nx_rv64im_imm_i(inst: i64) -> i64 { 130 // I-type: inst[31:20], sign-extended from bit 31. 131 let raw: i64 = (inst >> 20) & 0xfff 132 if (raw & 0x800) != 0 { 133 return raw | (0 - 4096) // sign-extend: bits 11..63 = 1 134 } 135 return raw 136} 137 138func nx_rv64im_imm_s(inst: i64) -> i64 { 139 // S-type: inst[31:25]<<5 | inst[11:7], sign-extended. 140 let hi: i64 = ((inst >> 25) & 0x7f) << 5 141 let lo: i64 = (inst >> 7) & 0x1f 142 let raw: i64 = hi | lo 143 if (raw & 0x800) != 0 { 144 return raw | (0 - 4096) 145 } 146 return raw 147} 148 149func nx_rv64im_imm_b(inst: i64) -> i64 { 150 // B-type: inst[31]<<12 | inst[7]<<11 | inst[30:25]<<5 | inst[11:8]<<1, sign-extended. 151 let b12: i64 = ((inst >> 31) & 0x1) << 12 152 let b11: i64 = ((inst >> 7) & 0x1) << 11 153 let b10_5: i64 = ((inst >> 25) & 0x3f) << 5 154 let b4_1: i64 = ((inst >> 8) & 0xf) << 1 155 let raw: i64 = b12 | b11 | b10_5 | b4_1 156 if (raw & 0x1000) != 0 { 157 return raw | (0 - 8192) 158 } 159 return raw 160} 161 162func nx_rv64im_imm_u(inst: i64) -> i64 { 163 // U-type (lui/auipc): the 20-bit immediate sits in inst[31:12], low 12 zero. RV64 SIGN-EXTENDS from bit 31 into 164 // bits [63:32] -- so a lui with bit 31 set is a NEGATIVE 64-bit value. (BUG FIX 2026-07-09: the old 165 // `(inst>>12)<<12` zero-extended, silently invisible in low bytes but WRONG for mulh/signed-div/slt/sra of a 166 // lui-derived negative -- caught by the QEMU differential fuzzer.) 167 let v: i64 = inst & 0xFFFFF000 // bits [31:12], [11:0]=0 (positive, < 2^32) 168 if (v & 0x80000000) != 0 { return v - 4294967296 } // sign-extend from bit 31 (subtract 2^32) 169 return v 170} 171 172func nx_rv64im_imm_j(inst: i64) -> i64 { 173 // J-type (jal): inst[31]<<20 | inst[19:12]<<12 | inst[20]<<11 | inst[30:21]<<1. 174 let b20: i64 = ((inst >> 31) & 0x1) << 20 175 let b19_12: i64 = ((inst >> 12) & 0xff) << 12 176 let b11: i64 = ((inst >> 20) & 0x1) << 11 177 let b10_1: i64 = ((inst >> 21) & 0x3ff) << 1 178 let raw: i64 = b20 | b19_12 | b11 | b10_1 179 if (raw & 0x100000) != 0 { 180 return raw | (0 - 2097152) 181 } 182 return raw 183} 184 185// ===== Top-level decode ================================================= 186// 187// Single-cycle decode: given a 32-bit instruction word, returns the 188// sealed-enum kind code. Operand fields (rd, rs1, rs2, immediate) 189// are extracted by the helpers above and used by the execute stage. 190 191func nx_rv64im_decode_kind(inst: i64) -> i64 { 192 let op: i64 = nx_rv64im_opcode(inst) 193 194 if op == 0x37 { return NX_RV64IM_OP_LUI } 195 if op == 0x17 { return NX_RV64IM_OP_AUIPC } 196 if op == 0x6f { return NX_RV64IM_OP_JAL } 197 if op == 0x67 { return NX_RV64IM_OP_JALR } 198 if op == 0x63 { return NX_RV64IM_OP_BRANCH } 199 if op == 0x03 { return NX_RV64IM_OP_LOAD } 200 if op == 0x23 { return NX_RV64IM_OP_STORE } 201 if op == 0x13 { return NX_RV64IM_OP_OP_IMM } 202 if op == 0x1b { return NX_RV64IM_OP_OP_IMM_32 } 203 if op == 0x0f { return NX_RV64IM_OP_FENCE } 204 if op == 0x73 { return NX_RV64IM_OP_SYSTEM } 205 if op == 0x2f { return NX_RV64IM_OP_AMO } // A extension: lr/sc + amo* (funct5 disambiguates) 206 207 // R-type 64-bit: M extension uses funct7=0000001; rest is base I. 208 if op == 0x33 { 209 let f7: i64 = nx_rv64im_funct7(inst) 210 if f7 == 0x01 { 211 let f3: i64 = nx_rv64im_funct3(inst) 212 if f3 < 4 { return NX_RV64IM_OP_M_MUL } 213 return NX_RV64IM_OP_M_DIV 214 } 215 return NX_RV64IM_OP_OP 216 } 217 218 // R-type 32-bit (W-suffix): M extension shares the funct7=0000001 overlay. 219 if op == 0x3b { 220 let f7: i64 = nx_rv64im_funct7(inst) 221 if f7 == 0x01 { 222 let f3: i64 = nx_rv64im_funct3(inst) 223 if f3 == 0 { return NX_RV64IM_OP_M_MUL_32 } 224 return NX_RV64IM_OP_M_DIV_32 225 } 226 return NX_RV64IM_OP_OP_32 227 } 228 229 return NX_RV64IM_OP_INVALID 230} 231 232// ===== HDL-graph builder (DSL use) ================================================= 233// 234// Builds the decoder as a NishiHDL module: takes a 32-bit input wire 235// (the instruction word from fetch), declares the per-field output 236// wires, and registers the decode logic on the module graph. 237// 238// V1 returns OK if the module-graph was built successfully; future 239// commits (nishi-sim, nishi-synth) traverse the module to emit 240// behaviour or gates. 241 242const NX_RV64IM_DECODER_WIDTH_INST: i64 = 32 // input instruction 243const NX_RV64IM_DECODER_WIDTH_OPKIND: i64 = 8 // sealed enum (fits 18 values) 244const NX_RV64IM_DECODER_WIDTH_REG: i64 = 5 // rs1/rs2/rd index 245const NX_RV64IM_DECODER_WIDTH_IMM: i64 = 64 // sign-extended 246 247// Handles published by nx_rv64im_decoder_build(); caller wires the 248// CPU's execute stage to these. 249struct NxRv64imDecoderPorts { 250 inst_in: i64 // input wire, 32-bit 251 op_kind: i64 // output wire, 8-bit (sealed enum) 252 rd: i64 // output wire, 5-bit 253 rs1: i64 // output wire, 5-bit 254 rs2: i64 // output wire, 5-bit 255 imm_signed: i64 // output wire, 64-bit 256} 257 258func nx_rv64im_decoder_build(m: *NxHdlModule, ports: *NxRv64imDecoderPorts) -> i64 { 259 let p_inst: i64 = nx_hdl_input(m, NX_RV64IM_DECODER_WIDTH_INST) 260 if p_inst < 0 { return p_inst } 261 let p_opkind: i64 = nx_hdl_output(m, NX_RV64IM_DECODER_WIDTH_OPKIND) 262 if p_opkind < 0 { return p_opkind } 263 let p_rd: i64 = nx_hdl_output(m, NX_RV64IM_DECODER_WIDTH_REG) 264 if p_rd < 0 { return p_rd } 265 let p_rs1: i64 = nx_hdl_output(m, NX_RV64IM_DECODER_WIDTH_REG) 266 if p_rs1 < 0 { return p_rs1 } 267 let p_rs2: i64 = nx_hdl_output(m, NX_RV64IM_DECODER_WIDTH_REG) 268 if p_rs2 < 0 { return p_rs2 } 269 let p_imm: i64 = nx_hdl_output(m, NX_RV64IM_DECODER_WIDTH_IMM) 270 if p_imm < 0 { return p_imm } 271 272 ports.inst_in = p_inst 273 ports.op_kind = p_opkind 274 ports.rd = p_rd 275 ports.rs1 = p_rs1 276 ports.rs2 = p_rs2 277 ports.imm_signed = p_imm 278 279 // V1: the actual combinational logic that drives p_opkind / 280 // p_rd / p_rs1 / p_rs2 / p_imm from p_inst lives in the 281 // simulator (nishi-sim, future commit) and the netlist emitter 282 // (nishi-synth, future commit), reading from this declared 283 // module shape. Today the module is structural-only: ports 284 // exist, no driver edges yet. 285 // 286 // Once nishi-sim lands, each port gets a per-cycle evaluator 287 // that calls nx_rv64im_decode_kind / nx_rv64im_rd / etc. on 288 // the latched instruction word. 289 290 return NX_HDL_OK 291}