Skip to main content

logicaffeine_compile/codegen_sva/
xpu_soc_rtl.rs

1//! xpu_soc_rtl — Stage 6 of the self-synthesizing bytecode XPU: the SoC that receives a block's BYTECODE OVER
2//! THE WIRE, parses it on-chip, and runs it on the reconfigurable execute engine — the north star made
3//! physical (in simulation). A block's decoded op list is serialized to a byte stream (the wire format); the
4//! SoC clocks the bytes in one at a time into an on-chip buffer, assembles them into the engine's config +
5//! inputs, and the engine computes the result — no host help beyond the bytes on the wire.
6//!
7//! Wire format (big-endian), matching `block_stream_bytes`: for each of `k` cells, `[ctrl:4][a_const:8]
8//! [b_const:8]` (20 bytes), then `[result_idx:2]`, then `in[j]:8` for each of `n_in` inputs. The SoC counts
9//! bytes into a buffer, raises `done` when the whole frame has arrived, and drives `result` combinationally
10//! from the assembled config. This is `nexys_soc.v`'s load-then-run pattern, but the payload configures the
11//! spatial execute engine instead of the sequential soft-core.
12//!
13//! Proven ≡ the VM under iverilog by `xpu_soc_lock`: for a corpus, streaming a program's bytecode bytes makes
14//! the SoC compute exactly what the VM computes. (The engine compute mirrors `block_engine_rtl` — a candidate
15//! for a shared core once the SoC grows the sequencer + state-plane BRAM.)
16
17use super::block_engine_rtl::BlockConfig;
18
19/// Bytes per cell in the wire frame: 4 (ctrl) + 8 (a const) + 8 (b const).
20const CELL_BYTES: usize = 20;
21
22/// Serialize a block's config + one input vector into the SoC's big-endian wire byte stream.
23pub fn block_stream_bytes(cfg: &BlockConfig, inputs: &[i64], k: usize, n_in: usize) -> Vec<u8> {
24    assert_eq!(cfg.ctrl.len(), k, "config must be padded to k cells");
25    let mut out = Vec::with_capacity(k * CELL_BYTES + 2 + n_in * 8);
26    for i in 0..k {
27        out.extend_from_slice(&cfg.ctrl[i].to_be_bytes()); // 4
28        out.extend_from_slice(&(cfg.ac[i] as u64).to_be_bytes()); // 8
29        out.extend_from_slice(&(cfg.bc[i] as u64).to_be_bytes()); // 8
30    }
31    out.extend_from_slice(&(cfg.result as u16).to_be_bytes()); // 2
32    for j in 0..n_in {
33        let v = if j < inputs.len() { inputs[j] } else { 0 };
34        out.extend_from_slice(&(v as u64).to_be_bytes()); // 8
35    }
36    out
37}
38
39/// A big-endian byte-concatenation `{mem[base], mem[base+1], ...}` of `n` bytes → a `[8n-1:0]` value.
40fn be_bytes(base: usize, n: usize) -> String {
41    let parts: Vec<String> = (0..n).map(|b| format!("mem[{}]", base + b)).collect();
42    format!("{{{}}}", parts.join(", "))
43}
44
45fn in_mux(field: &str, n_in: usize) -> String {
46    let mut acc = "64'sd0".to_string();
47    for j in (0..n_in).rev() {
48        acc = format!("({field}==4'd{j}) ? iv_{j} : ({acc})");
49    }
50    acc
51}
52
53fn v_mux(field: &str, upto: usize) -> String {
54    let mut acc = "64'sd0".to_string();
55    for j in (0..upto).rev() {
56        acc = format!("({field}==4'd{j}) ? v_{j} : ({acc})");
57    }
58    acc
59}
60
61/// Emit the XPU SoC: a byte-stream loader (`byte_in`/`byte_valid`) filling an on-chip frame buffer, on-chip
62/// assembly of the engine's config + inputs, and the `k`-cell execute engine — `done` + `result` when the
63/// whole frame has arrived.
64pub fn emit_xpu_soc_verilog(name: &str, k: usize, n_in: usize) -> String {
65    let total = k * CELL_BYTES + 2 + n_in * 8;
66    let res_base = k * CELL_BYTES;
67    let in_base = res_base + 2;
68
69    let mut asm = String::new();
70    // Per-cell config assembled from the frame buffer (big-endian).
71    for i in 0..k {
72        let base = i * CELL_BYTES;
73        asm.push_str(&format!("  wire [31:0] ct_{i} = {};\n", be_bytes(base, 4)));
74        asm.push_str(&format!(
75            "  wire signed [63:0] ac_{i} = {};\n",
76            be_bytes(base + 4, 8)
77        ));
78        asm.push_str(&format!(
79            "  wire signed [63:0] bc_{i} = {};\n",
80            be_bytes(base + 12, 8)
81        ));
82    }
83    asm.push_str(&format!("  wire [15:0] res = {};\n", be_bytes(res_base, 2)));
84    for j in 0..n_in {
85        asm.push_str(&format!(
86            "  wire signed [63:0] iv_{j} = {};\n",
87            be_bytes(in_base + j * 8, 8)
88        ));
89    }
90
91    // The execute engine over the assembled config (mirrors block_engine_rtl).
92    let mut comp = String::new();
93    for i in 0..k {
94        let ain = in_mux(&format!("ct_{i}[9:6]"), n_in);
95        let bin = in_mux(&format!("ct_{i}[15:12]"), n_in);
96        let av = if i == 0 {
97            "64'sd0".to_string()
98        } else {
99            v_mux(&format!("ct_{i}[9:6]"), i)
100        };
101        let bv = if i == 0 {
102            "64'sd0".to_string()
103        } else {
104            v_mux(&format!("ct_{i}[15:12]"), i)
105        };
106        comp.push_str(&format!(
107            "  wire signed [63:0] opa_{i} = (ct_{i}[5:4]==2'd0) ? ({ain}) : (ct_{i}[5:4]==2'd1) ? ac_{i} : ({av});\n"
108        ));
109        comp.push_str(&format!(
110            "  wire signed [63:0] opb_{i} = (ct_{i}[11:10]==2'd0) ? ({bin}) : (ct_{i}[11:10]==2'd1) ? bc_{i} : ({bv});\n"
111        ));
112        comp.push_str(&format!(
113            "  wire signed [63:0] v_{i} = alu_f(ct_{i}[3:0], opa_{i}, opb_{i});\n"
114        ));
115    }
116    let mut res_sel = "64'sd0".to_string();
117    for i in (0..k).rev() {
118        res_sel = format!("(res==16'd{i}) ? v_{i} : ({res_sel})");
119    }
120
121    format!(
122        "// xpu_{name} — the self-synthesizing bytecode XPU SoC. A block's bytecode is streamed in byte-by-byte\n\
123         // (byte_in/byte_valid), buffered on-chip, assembled into the reconfigurable engine's config + inputs,\n\
124         // and RUN — bytecode over the wire → result, no host help. (nexys_soc's load-then-run, payload = the\n\
125         // spatial execute engine's config instead of soft-core opcodes.) Proven ≡ VM by xpu_soc_lock.\n\
126         module xpu_{name}(input clk, input rst, input [7:0] byte_in, input byte_valid,\n\
127         \x20   output signed [63:0] result, output done);\n\
128         \x20 localparam integer TOTAL = {total};\n\
129         \x20 reg [7:0] mem [0:{tm1}];\n\
130         \x20 reg [15:0] cnt;\n\
131         \x20 reg loaded;\n\
132         \x20 always @(posedge clk) begin\n\
133         \x20   if (rst) begin cnt <= 16'd0; loaded <= 1'b0; end\n\
134         \x20   else if (!loaded && byte_valid) begin\n\
135         \x20     mem[cnt] <= byte_in;\n\
136         \x20     if (cnt == TOTAL-1) loaded <= 1'b1;\n\
137         \x20     cnt <= cnt + 16'd1;\n\
138         \x20   end\n\
139         \x20 end\n\
140         \x20 assign done = loaded;\n\
141         \x20 function signed [63:0] alu_f(input [3:0] op, input signed [63:0] a, input signed [63:0] b);\n\
142         \x20   case (op)\n\
143         \x20     4'd0: alu_f = a + b; 4'd1: alu_f = a - b; 4'd2: alu_f = a * b;\n\
144         \x20     4'd3: alu_f = a & b; 4'd4: alu_f = a | b; 4'd5: alu_f = a ^ b;\n\
145         \x20     4'd6: alu_f = a <<< b; 4'd7: alu_f = a >>> b; default: alu_f = 64'sd0;\n\
146         \x20   endcase\n\
147         \x20 endfunction\n\
148         {asm}{comp}  assign result = {res_sel};\n\
149         endmodule\n",
150        tm1 = total - 1,
151    )
152}
153
154/// A self-checking testbench: clock a program's bytecode frame in byte-by-byte, wait for `done`, and compare
155/// `result` to the VM oracle. One frame carries the config + one input vector, so a frame is streamed per
156/// (program, input) case; `rst` clears the loader between frames.
157pub fn emit_xpu_soc_testbench(name: &str, n_cases: usize, frame_len: usize) -> String {
158    format!(
159        r#"// tb_xpu_{name} — stream each program's bytecode frame in over the wire; result must equal the VM.
160`timescale 1ns/1ps
161module tb_xpu_{name};
162  reg clk = 1'b0;
163  reg rst;
164  reg [7:0] byte_in;
165  reg byte_valid;
166  wire signed [63:0] result;
167  wire done;
168  reg [7:0] frames [0:{flast}];   // n_cases frames concatenated, {frame_len} bytes each
169  reg signed [63:0] eout [0:{clast}];
170  integer i, b, errors, guard;
171  xpu_{name} dut(.clk(clk), .rst(rst), .byte_in(byte_in), .byte_valid(byte_valid), .result(result), .done(done));
172  always #5 clk = ~clk;
173  initial begin
174    errors = 0;
175    $readmemh("xpu_{name}_frames.hex", frames);
176    $readmemh("xpu_{name}_eout.hex", eout);
177    for (i = 0; i < {n_cases}; i = i + 1) begin
178      rst = 1'b1; byte_valid = 1'b0; @(posedge clk); #1;
179      rst = 1'b0;
180      for (b = 0; b < {frame_len}; b = b + 1) begin
181        byte_in = frames[i*{frame_len}+b]; byte_valid = 1'b1; @(posedge clk); #1;
182      end
183      byte_valid = 1'b0;
184      guard = 0;
185      while (done !== 1'b1 && guard < 100000) begin @(posedge clk); #1; guard = guard + 1; end
186      #1;
187      if (result !== eout[i]) begin errors = errors + 1; $display("FAIL case %0d: got %0d exp %0d", i, result, eout[i]); end
188    end
189    if (errors == 0) $display("PASS: {n_cases} bytecode frames run on the XPU SoC match the VM");
190    else $display("FAIL: %0d XPU SoC mismatches", errors);
191    $finish;
192  end
193endmodule
194"#,
195        flast = n_cases * frame_len - 1,
196        clast = n_cases - 1,
197    )
198}