Skip to main content

logicaffeine_compile/codegen_sva/
caravel_rtl.rs

1//! caravel_rtl — the Infinichip command player wrapped as an efabless **Caravel user macro**, the
2//! step that makes it literally shuttle-integratable.
3//!
4//! A real Caravel submission keeps efabless's golden `user_project_wrapper.v` VERBATIM and only
5//! swaps the user macro instantiated inside it. So the artifact WE own is that macro:
6//! **`dram_executor_wb`** — the countdown command player (`codegen_sva::dram`, decode + issue-timing
7//! certified by our own SAT) behind a standard **Wishbone** slave, with the issued command exposed
8//! on the **logic-analyzer** bus for observation. The management SoC writes the 64-bit compile-time
9//! plan word over Wishbone (two 32-bit stores), strobes launch, and the certified FSM issues.
10//!
11//! Register map (Wishbone byte offsets, caravel user base 0x3000_0000):
12//!
13//! ```text
14//! 0x00  W  cmd_word[31:0]           0x0C  R  {issued, bank, op} status
15//! 0x04  W  cmd_word[63:32]          0x10  R  row of the last issued command
16//! 0x08  W  bit0 = launch (cmd_valid)
17//! ```
18//!
19//! `la_data_out[63:0]` = {issued, 0, bank, op, row} — live observation on the logic analyzer.
20//!
21//! Wishbone conventions match `user_proj_example` (`valid = cyc & stb`, single-cycle `ack`).
22
23use super::dram::emit_dram_executor_verilog;
24
25/// The Wishbone slave that fronts the certified command player — the efabless Caravel USER MACRO.
26fn dram_executor_wb() -> &'static str {
27    r#"// dram_executor_wb — the certified DRAM command player behind a Wishbone slave (Caravel user
28// macro). The management SoC writes the compile-time plan word + a launch strobe; the countdown
29// FSM (proven decode + issue-timing) fires the command, captured for readback + logic-analyzer.
30module dram_executor_wb(
31`ifdef USE_POWER_PINS
32  inout         vccd1,   // User area 1.8V supply — Caravel PDN hook
33  inout         vssd1,   // User area digital ground
34`endif
35  input         wb_clk_i,
36  input         wb_rst_i,
37  input         wbs_stb_i,
38  input         wbs_cyc_i,
39  input         wbs_we_i,
40  input  [3:0]  wbs_sel_i,
41  input  [31:0] wbs_adr_i,
42  input  [31:0] wbs_dat_i,
43  output reg    wbs_ack_o,
44  output reg [31:0] wbs_dat_o,
45  output [63:0] la_data_out
46);
47  wire valid = wbs_cyc_i & wbs_stb_i;
48  wire wr    = valid & wbs_we_i;
49
50  reg  [63:0] cmd_word_r;
51  reg         cmd_valid_r;
52  wire        cmd_ack, issue;
53  wire [2:0]  op;  wire [4:0] bank;  wire [31:0] row;
54
55  // The certified countdown executor — instantiated unchanged.
56  dram_executor u_exec(
57    .clk(wb_clk_i), .rst(wb_rst_i),
58    .cmd_word(cmd_word_r), .cmd_valid(cmd_valid_r),
59    .cmd_ack(cmd_ack), .issue(issue), .op(op), .bank(bank), .row(row)
60  );
61
62  // Latch the last issued command for readback / observation.
63  reg [2:0] op_c;  reg [4:0] bank_c;  reg [31:0] row_c;  reg issued_c;
64
65  // POST-FAB HANDSHAKE. `issued_c` alone is a STICKY flag: set on the first issue and cleared
66  // only by reset. Firmware that writes a command and polls the status register therefore reads
67  // `issued = 1` left over from the PREVIOUS command and cannot tell its own command has gone
68  // out — and two identical back-to-back commands are indistinguishable in every visible field.
69  // The user area's only observation path after fabrication is the management SoC reading these
70  // registers, so that made command sequencing on real silicon unreliable.
71  //
72  // `issue_count` is the fix: a monotonically increasing counter, one tick per issued command.
73  // The protocol is read-modify-poll, with no clear-on-read race and no lost event:
74  //     n = read(0x0c)[15:8];  write cmd_word; write cmd_valid;
75  //     poll 0x0c until [15:8] != n;  then read op/bank from 0x0c and row from 0x10.
76  // It wraps at 256 by design — only INEQUALITY is ever tested, so wrapping is harmless unless
77  // firmware sleeps through 256 issues, which the single-command-at-a-time protocol prevents.
78  // `issued_c` is retained at its original bit position so existing readers keep working.
79  reg [7:0] issue_count;
80
81  always @(posedge wb_clk_i) begin
82    if (wb_rst_i) begin
83      cmd_word_r <= 64'd0; cmd_valid_r <= 1'b0; wbs_ack_o <= 1'b0;
84      op_c <= 3'd0; bank_c <= 5'd0; row_c <= 32'd0; issued_c <= 1'b0;
85      issue_count <= 8'd0;
86    end else begin
87      wbs_ack_o <= valid & ~wbs_ack_o;         // single-cycle Wishbone ack
88      if (cmd_ack) cmd_valid_r <= 1'b0;         // the executor consumed the word
89      if (issue) begin
90        op_c <= op; bank_c <= bank; row_c <= row; issued_c <= 1'b1;
91        issue_count <= issue_count + 8'd1;   // the observable event firmware polls on
92      end
93      if (wr) begin
94        case (wbs_adr_i[7:0])
95          8'h00: cmd_word_r[31:0]  <= wbs_dat_i;
96          8'h04: cmd_word_r[63:32] <= wbs_dat_i;
97          8'h08: cmd_valid_r       <= wbs_dat_i[0];
98        endcase
99      end
100    end
101  end
102
103  // Registered read data mux.
104  always @(posedge wb_clk_i) begin
105    case (wbs_adr_i[7:0])
106      8'h0c:   wbs_dat_o <= {issued_c, 15'd0, issue_count, bank_c, op_c};
107      8'h10:   wbs_dat_o <= row_c;
108      default: wbs_dat_o <= 32'd0;
109    endcase
110  end
111
112  assign la_data_out = {issued_c, 15'd0, issue_count, bank_c, op_c, row_c};
113endmodule
114"#
115}
116
117/// Emit the SELF-CONTAINED user macro: the certified `dram_executor` + the `dram_executor_wb`
118/// Wishbone front end. This is the design hardened + golden-DRC'd + dropped into Caravel's golden
119/// `user_project_wrapper` for a shuttle submission.
120pub fn emit_dram_executor_wb_verilog() -> String {
121    let mut v = String::new();
122    v.push_str(&emit_dram_executor_verilog());
123    v.push('\n');
124    v.push_str(dram_executor_wb());
125    v
126}
127
128/// A self-checking Wishbone testbench: the "management SoC" writes a delta-0 plan word
129/// (op=1, bank=3, row=7) over Wishbone, strobes launch, and asserts the certified FSM issues that
130/// exact command — observed on `la_data_out`. Prints PASS/FAIL; no external vectors.
131pub fn emit_dram_executor_wb_testbench() -> String {
132    r#"// tb_dram_executor_wb — drive the command player over Wishbone; check it issues the command.
133`timescale 1ns/1ps
134module tb_dram_executor_wb;
135  reg clk = 0; always #5 clk = ~clk;
136  reg rst = 1;
137  reg stb = 0, cyc = 0, we = 0;
138  reg [3:0] sel = 4'hf;
139  reg [31:0] adr = 0, dat = 0;
140  wire ack;  wire [31:0] rdat;  wire [63:0] la;
141  integer errors = 0;
142
143  dram_executor_wb dut(
144    .wb_clk_i(clk), .wb_rst_i(rst), .wbs_stb_i(stb), .wbs_cyc_i(cyc), .wbs_we_i(we),
145    .wbs_sel_i(sel), .wbs_adr_i(adr), .wbs_dat_i(dat), .wbs_ack_o(ack), .wbs_dat_o(rdat),
146    .la_data_out(la)
147  );
148
149  task wb_write(input [31:0] a, input [31:0] d);
150    begin
151      @(posedge clk); adr <= a; dat <= d; we <= 1; stb <= 1; cyc <= 1;
152      @(posedge clk); while (!ack) @(posedge clk);
153      stb <= 0; cyc <= 0; we <= 0;
154      @(posedge clk);
155    end
156  endtask
157
158  initial begin
159    repeat (3) @(posedge clk); rst = 0; @(posedge clk);
160    // delta-0 command: cmd_word = {24'd0, op=1, bank=3, row=7}
161    wb_write(32'h00, 32'd7);       // cmd_word[31:0]  = row
162    wb_write(32'h04, 32'h23);      // cmd_word[63:32] = {delta=0, op=1(<<5), bank=3}
163    wb_write(32'h08, 32'h1);       // launch (cmd_valid)
164    repeat (8) @(posedge clk);
165    // la_data_out = {issued, 15'd0, issue_count, bank, op, row}
166    if (la[63] !== 1'b1)        begin errors = errors + 1; $display("FAIL: never issued"); end
167    if (la[47:40] !== 8'd1)     begin errors = errors + 1; $display("FAIL: issue_count %0d != 1 after one command", la[47:40]); end
168    if (la[39:35] !== 5'd3)     begin errors = errors + 1; $display("FAIL: bank %0d != 3", la[39:35]); end
169    if (la[34:32] !== 3'd1)     begin errors = errors + 1; $display("FAIL: op %0d != 1", la[34:32]); end
170    if (la[31:0] !== 32'd7)     begin errors = errors + 1; $display("FAIL: row %0d != 7", la[31:0]); end
171    if (errors == 0) $display("PASS: Wishbone-driven command player issued op=1 bank=3 row=7");
172    else $display("FAIL: %0d Wishbone integration errors", errors);
173    $finish;
174  end
175endmodule
176"#
177    .to_string()
178}
179
180/// The FULL-INTERFACE Caravel user macro (`dram_user_project`) — exposes the ENTIRE Caravel user
181/// interface (Wishbone + 128-bit logic analyzer + 38 GPIO + user_irq + user_clock2) so the golden
182/// wrapper can connect every pin 1:1 (LVS-clean), with the locked, certified `dram_executor_wb`
183/// Wishbone core inside. The command player drives the low logic-analyzer bits; the rest of the
184/// interface is a proper Caravel passthrough (unused inputs kept + connected, unused outputs tied).
185fn dram_user_project() -> &'static str {
186    r#"// dram_user_project — the full-interface Caravel user macro: the certified Wishbone command
187// player (dram_executor_wb) presented on the complete Caravel user interface so the wrapper wires
188// every pin (LVS-clean). The command player issues onto la_data_out[63:0]; other I/O is passthrough.
189module dram_user_project (
190`ifdef USE_POWER_PINS
191  inout         vccd1,
192  inout         vssd1,
193`endif
194  input         wb_clk_i,
195  input         wb_rst_i,
196  input         wbs_stb_i,
197  input         wbs_cyc_i,
198  input         wbs_we_i,
199  input  [3:0]  wbs_sel_i,
200  input  [31:0] wbs_dat_i,
201  input  [31:0] wbs_adr_i,
202  output        wbs_ack_o,
203  output [31:0] wbs_dat_o,
204  input  [127:0] la_data_in,
205  output [127:0] la_data_out,
206  input  [127:0] la_oenb,
207  input  [37:0]  io_in,
208  output [37:0]  io_out,
209  output [37:0]  io_oeb,
210  input         user_clock2,
211  output [2:0]  user_irq
212);
213  wire [63:0] la_status;
214  dram_executor_wb core (
215`ifdef USE_POWER_PINS
216    .vccd1(vccd1), .vssd1(vssd1),
217`endif
218    .wb_clk_i(wb_clk_i), .wb_rst_i(wb_rst_i),
219    .wbs_stb_i(wbs_stb_i), .wbs_cyc_i(wbs_cyc_i), .wbs_we_i(wbs_we_i),
220    .wbs_sel_i(wbs_sel_i), .wbs_adr_i(wbs_adr_i), .wbs_dat_i(wbs_dat_i),
221    .wbs_ack_o(wbs_ack_o), .wbs_dat_o(wbs_dat_o),
222    .la_data_out(la_status)
223  );
224  assign la_data_out = {64'd0, la_status};
225  assign io_out   = 38'd0;
226  assign io_oeb   = {38{1'b1}};   // all user GPIO as inputs (output-enable-bar high)
227  assign user_irq = 3'd0;
228  // The remaining Caravel inputs are part of the fixed interface (wired 1:1 by the wrapper for a
229  // clean LVS) and unused by the command player; reduce them so the ports are kept + connected.
230  wire _unused = &{1'b0, la_data_in, la_oenb, io_in, user_clock2};
231endmodule
232"#
233}
234
235/// Emit the SELF-CONTAINED full-interface user macro: `dram_executor` + `dram_executor_wb` core +
236/// `dram_user_project`. This is the macro hardened (with power pins + tap insertion) and placed as
237/// `mprj` inside the golden `user_project_wrapper` for a shuttle submission.
238pub fn emit_dram_user_project_verilog() -> String {
239    let mut v = emit_dram_executor_wb_verilog();
240    v.push('\n');
241    v.push_str(dram_user_project());
242    v
243}
244
245/// Emit our `user_project_wrapper` for the DRAM command player: [`emit_user_project_wrapper_around`]
246/// `dram_user_project`.
247pub fn emit_user_project_wrapper_verilog() -> String {
248    emit_user_project_wrapper_around("dram_user_project", "certified command player")
249}
250
251/// Emit a `user_project_wrapper` — the efabless golden Caravel wrapper interface, verbatim ports,
252/// instantiating the full-interface user macro `macro_module` as `mprj` and connecting EVERY wrapper
253/// pin 1:1 (the passthrough that makes the placed-and-routed wrapper LVS-clean). `carries` names what
254/// the macro holds, for the header.
255pub fn emit_user_project_wrapper_around(macro_module: &str, carries: &str) -> String {
256    r#"// user_project_wrapper — the Caravel user area. Golden interface verbatim; the full-interface
257// user macro ({macro_module}, {carries} inside) is instantiated as `mprj` with
258// every pin connected 1:1 so the hardened wrapper is LVS-clean.
259`define MPRJ_IO_PADS 38
260module user_project_wrapper #(parameter BITS = 32) (
261`ifdef USE_POWER_PINS
262  inout vdda1, inout vdda2, inout vssa1, inout vssa2,
263  inout vccd1, inout vccd2, inout vssd1, inout vssd2,
264`endif
265  input         wb_clk_i,
266  input         wb_rst_i,
267  input         wbs_stb_i,
268  input         wbs_cyc_i,
269  input         wbs_we_i,
270  input  [3:0]  wbs_sel_i,
271  input  [31:0] wbs_dat_i,
272  input  [31:0] wbs_adr_i,
273  output        wbs_ack_o,
274  output [31:0] wbs_dat_o,
275  input  [127:0] la_data_in,
276  output [127:0] la_data_out,
277  input  [127:0] la_oenb,
278  input  [`MPRJ_IO_PADS-1:0] io_in,
279  output [`MPRJ_IO_PADS-1:0] io_out,
280  output [`MPRJ_IO_PADS-1:0] io_oeb,
281  inout  [`MPRJ_IO_PADS-10:0] analog_io,
282  input         user_clock2,
283  output [2:0]  user_irq
284);
285  // Full-interface user macro — the {carries} inside, EVERY pin wired 1:1.
286  {macro_module} mprj (
287`ifdef USE_POWER_PINS
288    .vccd1(vccd1), .vssd1(vssd1),
289`endif
290    .wb_clk_i(wb_clk_i), .wb_rst_i(wb_rst_i),
291    .wbs_stb_i(wbs_stb_i), .wbs_cyc_i(wbs_cyc_i), .wbs_we_i(wbs_we_i),
292    .wbs_sel_i(wbs_sel_i), .wbs_adr_i(wbs_adr_i), .wbs_dat_i(wbs_dat_i),
293    .wbs_ack_o(wbs_ack_o), .wbs_dat_o(wbs_dat_o),
294    .la_data_in(la_data_in), .la_data_out(la_data_out), .la_oenb(la_oenb),
295    .io_in(io_in), .io_out(io_out), .io_oeb(io_oeb),
296    .user_clock2(user_clock2), .user_irq(user_irq)
297  );
298  // analog_io has no digital use in this macro (left unconnected, as in the empty golden wrapper).
299endmodule
300"#
301    .replace("{macro_module}", macro_module)
302    .replace("{carries}", carries)
303}
304
305#[cfg(test)]
306#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/caravel_rtl/tests.rs"]
307mod tests;