Skip to main content

logicaffeine_compile/spatial/lower/
fabric_board.rs

1//! A fabric decode step as a board image: the step's RTL behind its host port, a serial line in front of the port,
2//! and the autoregressive loop run on chip.
3//!
4//! The image serves a byte protocol over 8N1 serial, every multi-byte field little-endian:
5//!
6//! | Command | Arguments | Reply |
7//! |---|---|---|
8//! | `I` identify | — | `i`, `L O G S`, the memory read latency |
9//! | `W` write a word | tensor (2), word (4), value (8) | `k` |
10//! | `R` read a word | tensor (2), word (4) | `r`, value (8) |
11//! | `S` one step | token (2), position (2) | `s`, next token (2), the step's cycles (4) |
12//! | `G` generate | count (2) | the next token (2) per step, then `e` and the cycles of the whole loop (4) |
13//!
14//! A step writes the token and the position into the step's tensors, resets and starts the fabric, waits for it, and
15//! reads the next token back; the cache the step writes stays in its memories. After a step the board holds the token
16//! it produced and the position after it, so `G` continues from the last `S`: it runs the steps itself, each one's
17//! token the one before it produced and its position the next, and its cycles are the chip's own decode time
18//! ([`generate_cycles`]) — the serial line carries two bytes per token and never stalls it while its queue has room.
19//! Tokens and positions are the values the step's tensors hold (the program's 1-based ones).
20
21use super::SpatialLowerError;
22use crate::spatial::ir::SpatialProgram;
23use crate::spatial::lower::fabric::{host_word, VerilogArtifact};
24
25/// Where a decode step's token, position and next token live: each a tensor and the word of its element `(0, 0)`.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct DecodeWords {
28    pub tok: (usize, usize),
29    pub pos: (usize, usize),
30    pub next: (usize, usize),
31}
32
33impl DecodeWords {
34    /// The words of the step's tensors named `tok`, `pos` and `next`.
35    pub fn of(p: &SpatialProgram, tok: &str, pos: &str, next: &str) -> Result<DecodeWords, SpatialLowerError> {
36        let word = |name: &str| -> Result<(usize, usize), SpatialLowerError> {
37            let t = p
38                .tensors
39                .iter()
40                .find(|t| t.name == name)
41                .ok_or(SpatialLowerError::Unsupported("a decode step without its token, position or next-token tensor"))?;
42            Ok((t.id.idx(), host_word(p, t, 0, 0)))
43        };
44        Ok(DecodeWords { tok: word(tok)?, pos: word(pos)?, next: word(next)? })
45    }
46}
47
48/// The cycles `G` reports for steps that took `step_cycles` each: per step, the protocol's thirteen states around the
49/// step — `G_LOOP`, the token and position writes, the reset, the start, the cycle the fabric's count ends on, the next
50/// token's read and its `lat` cycles, `G_TOK`, and the four that queue the token's two bytes. The `G_LOOP` that finds no
51/// step left reports the count as it stands before that cycle.
52pub fn generate_cycles(step_cycles: &[u64], lat: u64) -> u64 {
53    step_cycles.iter().map(|c| c + 13 + lat).sum()
54}
55
56/// A board image's core clock: the 100 MHz oscillator through a PLLE2, its VCO at `100 · mult / div` MHz and the core
57/// at the VCO over `out`.
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub struct CoreClock {
60    pub mult: u32,
61    pub div: u32,
62    pub out: u32,
63}
64
65impl CoreClock {
66    pub fn vco_mhz(self) -> f64 {
67        100.0 * self.mult as f64 / self.div as f64
68    }
69
70    pub fn mhz(self) -> f64 {
71        self.vco_mhz() / self.out as f64
72    }
73
74    /// Whether a PLLE2 of a speed grade -1 Artix-7 takes these settings (DS181): a multiplier of 2–64, an input divider
75    /// of 1–56 and an output divider of 1–128, the phase detector at 19–450 MHz and the VCO at 800–1600 MHz.
76    pub fn legal(self) -> bool {
77        let pfd = 100.0 / self.div as f64;
78        (2..=64).contains(&self.mult)
79            && (1..=56).contains(&self.div)
80            && (1..=128).contains(&self.out)
81            && (19.0..=450.0).contains(&pfd)
82            && (800.0..=1600.0).contains(&self.vco_mhz())
83    }
84
85    /// The fastest legal core clock whose period is at least `period_ns`; of clocks as fast, the one with the smallest
86    /// input divider, then the slowest VCO.
87    pub fn fastest_within(period_ns: f64) -> Option<CoreClock> {
88        let mut best: Option<CoreClock> = None;
89        for div in 1..=56 {
90            for mult in 2..=64 {
91                for out in 1..=128 {
92                    let c = CoreClock { mult, div, out };
93                    if !c.legal() || 1000.0 / c.mhz() < period_ns {
94                        continue;
95                    }
96                    let better = match best {
97                        None => true,
98                        Some(b) => c.mhz() > b.mhz() + 1e-9 || ((c.mhz() - b.mhz()).abs() <= 1e-9 && (c.div, c.vco_mhz() as u64) < (b.div, b.vco_mhz() as u64)),
99                    };
100                    if better {
101                        best = Some(c);
102                    }
103                }
104            }
105        }
106        best
107    }
108
109    /// The core clock's own allowance a route does not model: the PLL's output jitter and the clock tree's skew.
110    pub const UNCERTAINTY_NS: f64 = 0.5;
111
112    /// The fastest legal core clock a route closes under the clock contract: its period at least the routed path
113    /// (`1000 / fmax_mhz`), the residual the router bounds but does not time (`residual_ns`) and
114    /// [`Self::UNCERTAINTY_NS`].
115    pub fn for_route(fmax_mhz: f64, residual_ns: f64) -> Option<CoreClock> {
116        CoreClock::fastest_within(1000.0 / fmax_mhz + residual_ns + CoreClock::UNCERTAINTY_NS)
117    }
118}
119
120/// The serial line's bit time in core clocks at 115200 baud.
121pub fn clks_per_bit(clock: CoreClock) -> u32 {
122    (clock.mhz() * 1e6 / 115_200.0).round() as u32
123}
124
125/// An 8N1 receiver and transmitter: the receiver samples each bit at its middle and raises `valid` for one cycle with
126/// a byte whose stop bit held; the transmitter sends `data` when `go` is raised while it is idle.
127pub fn uart_verilog() -> String {
128    String::from(
129        "module uart_rx #(parameter CLKS_PER_BIT = 868) (input clk, input rst, input rx, output reg valid, output reg [7:0] data);
130  reg [1:0] sync = 2'b11;
131  always @(posedge clk) sync <= {sync[0], rx};
132  wire line = sync[1];
133  reg [1:0] st; reg [15:0] cnt; reg [2:0] bit_i; reg [7:0] sh;
134  always @(posedge clk) begin
135    valid <= 1'b0;
136    if (rst) begin st <= 2'd0; cnt <= 16'd0; bit_i <= 3'd0; end
137    else case (st)
138      2'd0: if (!line) begin st <= 2'd1; cnt <= 16'd0; end
139      2'd1: if (cnt == CLKS_PER_BIT / 2 - 1) begin cnt <= 16'd0; bit_i <= 3'd0; st <= line ? 2'd0 : 2'd2; end else cnt <= cnt + 16'd1;
140      2'd2: if (cnt == CLKS_PER_BIT - 1) begin cnt <= 16'd0; sh <= {line, sh[7:1]}; if (bit_i == 3'd7) st <= 2'd3; else bit_i <= bit_i + 3'd1; end else cnt <= cnt + 16'd1;
141      2'd3: if (cnt == CLKS_PER_BIT - 1) begin cnt <= 16'd0; st <= 2'd0; if (line) begin valid <= 1'b1; data <= sh; end end else cnt <= cnt + 16'd1;
142    endcase
143  end
144endmodule
145module uart_tx #(parameter CLKS_PER_BIT = 868) (input clk, input rst, input go, input [7:0] data, output reg tx, output busy);
146  reg [1:0] st; reg [15:0] cnt; reg [2:0] bit_i; reg [7:0] sh;
147  assign busy = st != 2'd0;
148  always @(posedge clk) begin
149    if (rst) begin st <= 2'd0; tx <= 1'b1; cnt <= 16'd0; bit_i <= 3'd0; end
150    else case (st)
151      2'd0: begin tx <= 1'b1; if (go) begin sh <= data; st <= 2'd1; cnt <= 16'd0; end end
152      2'd1: begin tx <= 1'b0; if (cnt == CLKS_PER_BIT - 1) begin cnt <= 16'd0; bit_i <= 3'd0; st <= 2'd2; end else cnt <= cnt + 16'd1; end
153      2'd2: begin tx <= sh[bit_i]; if (cnt == CLKS_PER_BIT - 1) begin cnt <= 16'd0; if (bit_i == 3'd7) st <= 2'd3; else bit_i <= bit_i + 3'd1; end else cnt <= cnt + 16'd1; end
154      2'd3: begin tx <= 1'b1; if (cnt == CLKS_PER_BIT - 1) begin cnt <= 16'd0; st <= 2'd0; end else cnt <= cnt + 16'd1; end
155    endcase
156  end
157endmodule
158",
159    )
160}
161
162/// A board's serial pair and the 128-byte reply queue that drains into its transmitter: what `decode_board` and
163/// `chip_board` share. The protocol pushes a reply byte with `q_push`/`q_byte` while `q_room`.
164const REPLY_QUEUE: &str = "  // The serial line and the reply queue it drains.
165  wire rx_v; wire [7:0] rx_b; wire tx_busy; reg tx_go; reg [7:0] tx_byte;
166  uart_rx #(CLKS_PER_BIT) urx(.clk(clk), .rst(rst), .rx(uart_rx), .valid(rx_v), .data(rx_b));
167  uart_tx #(CLKS_PER_BIT) utx(.clk(clk), .rst(rst), .go(tx_go), .data(tx_byte), .tx(uart_tx), .busy(tx_busy));
168  (* ram_style = \"registers\" *) reg [7:0] txq [0:127];
169  reg [6:0] tq_w, tq_r; reg [7:0] tq_n; reg q_push; reg [7:0] q_byte;
170  wire q_room = tq_n < 8'd127;
171  wire q_pop = !tx_busy && !tx_go && tq_n != 8'd0;
172  always @(posedge clk) begin
173    tx_go <= 1'b0;
174    if (rst) begin tq_w <= 7'd0; tq_r <= 7'd0; tq_n <= 8'd0; end
175    else begin
176      if (q_push) begin txq[tq_w] <= q_byte; tq_w <= tq_w + 7'd1; end
177      if (q_pop) begin tx_go <= 1'b1; tx_byte <= txq[tq_r]; tq_r <= tq_r + 7'd1; end
178      tq_n <= tq_n + (q_push ? 8'd1 : 8'd0) - (q_pop ? 8'd1 : 8'd0);
179    end
180  end
181";
182
183/// `decode_board`: the step (`spatial_top`), its host port driven by the protocol, the serial pair and a 128-byte reply
184/// queue. `lat` is the step's memory read latency, the cycles a host read waits for its word.
185pub fn decode_board_verilog(w: DecodeWords, lat: u64) -> String {
186    let mut s = uart_verilog();
187    s.push_str(&format!(
188        "module decode_board #(parameter CLKS_PER_BIT = 868) (input clk, input rst, input uart_rx, output uart_tx, output [7:0] led);
189  localparam LAT = {lat};
190  localparam [15:0] TOK_T = 16'd{tok_t}, POS_T = 16'd{pos_t}, NEXT_T = 16'd{next_t};
191  localparam [31:0] TOK_W = 32'd{tok_w}, POS_W = 32'd{pos_w}, NEXT_W = 32'd{next_w};
192  // The step.
193  reg f_rst, f_start; wire f_done; wire [31:0] f_cycles;
194  reg h_we, h_re; reg [15:0] h_tensor; reg [31:0] h_addr; reg [63:0] h_wdata; wire [63:0] h_rdata;
195  spatial_top fab(.clk(clk), .rst(rst | f_rst), .start(f_start), .done(f_done), .cycles(f_cycles), .host_we(h_we), .host_re(h_re),
196    .host_site(16'd0), .host_tensor(h_tensor), .host_addr(h_addr), .host_wdata(h_wdata), .host_rdata(h_rdata));
197",
198        tok_t = w.tok.0,
199        tok_w = w.tok.1,
200        pos_t = w.pos.0,
201        pos_w = w.pos.1,
202        next_t = w.next.0,
203        next_w = w.next.1,
204    ));
205    s.push_str(REPLY_QUEUE);
206    s.push_str(&format!(
207        "  // The protocol.
208  localparam C_IDLE = 5'd0, C_ARGS = 5'd1, C_EXEC = 5'd2, C_REPLY = 5'd3, C_WROTE = 5'd4, C_READW = 5'd5,
209             ST_WTOK = 5'd6, ST_WPOS = 5'd7, ST_RST = 5'd8, ST_START = 5'd9, ST_WAIT = 5'd10, ST_READ = 5'd11, ST_READW = 5'd12,
210             S_DONE = 5'd13, G_LOOP = 5'd14, G_TOK = 5'd15;
211  reg [4:0] st, ret, rret; reg [7:0] cmd; reg [3:0] argi, argc; reg [7:0] args [0:13];
212  reg [7:0] rb [0:8]; reg [3:0] ri, rlen; reg [3:0] lcnt;
213  reg [15:0] tok_r, pos_r, next_r, gen_n; reg [31:0] step_cycles, g_cycles; reg g_on;
214  wire [15:0] arg16_0 = {{args[1], args[0]}}, arg16_2 = {{args[3], args[2]}};
215  wire [31:0] arg32_2 = {{args[5], args[4], args[3], args[2]}};
216  wire [63:0] arg64_6 = {{args[13], args[12], args[11], args[10], args[9], args[8], args[7], args[6]}};
217  function [3:0] nargs(input [7:0] c);
218    case (c) 8'h57: nargs = 4'd14; 8'h52: nargs = 4'd6; 8'h53: nargs = 4'd4; 8'h47: nargs = 4'd2; default: nargs = 4'd0; endcase
219  endfunction
220  assign led = {{g_on, st[4:0], tq_n != 8'd0, f_done}};
221  always @(posedge clk) begin
222    q_push <= 1'b0; h_we <= 1'b0; f_rst <= 1'b0; f_start <= 1'b0;
223    if (g_on) g_cycles <= g_cycles + 32'd1;
224    if (rst) begin st <= C_IDLE; h_re <= 1'b0; g_on <= 1'b0; tok_r <= 16'd0; pos_r <= 16'd0; end
225    else case (st)
226      C_IDLE: if (rx_v) begin
227        cmd <= rx_b; argi <= 4'd0; argc <= nargs(rx_b);
228        st <= (nargs(rx_b) == 4'd0) ? C_EXEC : C_ARGS;
229      end
230      C_ARGS: if (rx_v) begin
231        args[argi] <= rx_b; argi <= argi + 4'd1;
232        if (argi + 4'd1 == argc) st <= C_EXEC;
233      end
234      C_EXEC: case (cmd)
235        8'h49: begin rb[0] <= 8'h69; rb[1] <= 8'h4c; rb[2] <= 8'h4f; rb[3] <= 8'h47; rb[4] <= 8'h53; rb[5] <= LAT;
236                     rlen <= 4'd6; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY; end
237        8'h57: begin h_we <= 1'b1; h_tensor <= arg16_0; h_addr <= arg32_2; h_wdata <= arg64_6; st <= C_WROTE; end
238        8'h52: begin h_re <= 1'b1; h_tensor <= arg16_0; h_addr <= arg32_2; lcnt <= LAT; st <= C_READW; end
239        8'h53: begin tok_r <= arg16_0; pos_r <= arg16_2; ret <= S_DONE; st <= ST_WTOK; end
240        8'h47: begin gen_n <= arg16_0; g_cycles <= 32'd0; g_on <= 1'b1; st <= G_LOOP; end
241        default: begin rb[0] <= 8'h3f; rlen <= 4'd1; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY; end
242      endcase
243      C_REPLY: if (ri == rlen) st <= rret;
244               else if (q_room && !q_push) begin q_push <= 1'b1; q_byte <= rb[ri]; ri <= ri + 4'd1; end
245      C_WROTE: begin rb[0] <= 8'h6b; rlen <= 4'd1; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY; end
246      C_READW: if (lcnt == 4'd0) begin
247        h_re <= 1'b0; rb[0] <= 8'h72;
248        rb[1] <= h_rdata[7:0]; rb[2] <= h_rdata[15:8]; rb[3] <= h_rdata[23:16]; rb[4] <= h_rdata[31:24];
249        rb[5] <= h_rdata[39:32]; rb[6] <= h_rdata[47:40]; rb[7] <= h_rdata[55:48]; rb[8] <= h_rdata[63:56];
250        rlen <= 4'd9; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY;
251      end else lcnt <= lcnt - 4'd1;
252      // One step: the token and the position in, a reset, a start, the run, the next token out.
253      ST_WTOK: begin h_we <= 1'b1; h_tensor <= TOK_T; h_addr <= TOK_W; h_wdata <= {{48'd0, tok_r}}; st <= ST_WPOS; end
254      ST_WPOS: begin h_we <= 1'b1; h_tensor <= POS_T; h_addr <= POS_W; h_wdata <= {{48'd0, pos_r}}; st <= ST_RST; end
255      ST_RST: begin f_rst <= 1'b1; st <= ST_START; end
256      ST_START: begin f_start <= 1'b1; st <= ST_WAIT; end
257      ST_WAIT: if (f_done && !f_start) begin step_cycles <= f_cycles; st <= ST_READ; end
258      ST_READ: begin h_re <= 1'b1; h_tensor <= NEXT_T; h_addr <= NEXT_W; lcnt <= LAT; st <= ST_READW; end
259      ST_READW: if (lcnt == 4'd0) begin h_re <= 1'b0; next_r <= h_rdata[15:0]; st <= ret; end else lcnt <= lcnt - 4'd1;
260      S_DONE: begin
261        tok_r <= next_r; pos_r <= pos_r + 16'd1;
262        rb[0] <= 8'h73; rb[1] <= next_r[7:0]; rb[2] <= next_r[15:8];
263        rb[3] <= step_cycles[7:0]; rb[4] <= step_cycles[15:8]; rb[5] <= step_cycles[23:16]; rb[6] <= step_cycles[31:24];
264        rlen <= 4'd7; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY;
265      end
266      // Generation: each step's token is the one before it produced, its position the next.
267      G_LOOP: if (gen_n == 16'd0) begin
268        g_on <= 1'b0; rb[0] <= 8'h65;
269        rb[1] <= g_cycles[7:0]; rb[2] <= g_cycles[15:8]; rb[3] <= g_cycles[23:16]; rb[4] <= g_cycles[31:24];
270        rlen <= 4'd5; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY;
271      end else begin ret <= G_TOK; st <= ST_WTOK; end
272      G_TOK: begin
273        rb[0] <= next_r[7:0]; rb[1] <= next_r[15:8]; rlen <= 4'd2; ri <= 4'd0;
274        tok_r <= next_r; pos_r <= pos_r + 16'd1; gen_n <= gen_n - 16'd1; rret <= G_LOOP; st <= C_REPLY;
275      end
276      default: st <= C_IDLE;
277    endcase
278  end
279endmodule
280"
281    ));
282    s
283}
284
285/// `chip_board`: one chip's program of a program split over chips ([`compile_program_to_chips`]), its host port and its
286/// halts driven over the serial pair. Beside `I`, `W` and `R` as `decode_board` serves them:
287///
288/// | Command | Arguments | Reply |
289/// |---|---|---|
290/// | `X` run | — | `x`, the link it halted at (2), or `0xFFFF` at its end, then the cycles it has run (4) |
291///
292/// `X` starts a step — a reset and a start — when none is running, and otherwise continues the halted fabric past its
293/// halt; either way it replies once the fabric halts again or finishes. The cycles pause while the fabric is halted, so
294/// each reply counts the cycles the program has run ([`fabric_cycle_segments`](super::fabric::fabric_cycle_segments)).
295/// A halted fabric's memories are still, so the host reads what the chip sends and writes what it receives with `R` and
296/// `W` between runs.
297///
298/// [`compile_program_to_chips`]: crate::spatial::pipeline::compile_program_to_chips
299pub fn chip_board_verilog(lat: u64) -> String {
300    let mut s = uart_verilog();
301    s.push_str(&format!(
302        "module chip_board #(parameter CLKS_PER_BIT = 868) (input clk, input rst, input uart_rx, output uart_tx, output [7:0] led);
303  localparam LAT = {lat};
304  // The chip's program.
305  reg f_rst, f_start, f_go; wire f_done, f_halted; wire [15:0] f_halt_at; wire [31:0] f_cycles;
306  reg h_we, h_re; reg [15:0] h_tensor; reg [31:0] h_addr; reg [63:0] h_wdata; wire [63:0] h_rdata;
307  spatial_top fab(.clk(clk), .rst(rst | f_rst), .start(f_start), .done(f_done), .cycles(f_cycles), .host_we(h_we), .host_re(h_re),
308    .host_site(16'd0), .host_tensor(h_tensor), .host_addr(h_addr), .host_wdata(h_wdata), .host_rdata(h_rdata),
309    .link_go(f_go), .halted(f_halted), .halt_at(f_halt_at));
310"
311    ));
312    s.push_str(REPLY_QUEUE);
313    s.push_str(
314        "  // The protocol.
315  localparam C_IDLE = 4'd0, C_ARGS = 4'd1, C_EXEC = 4'd2, C_REPLY = 4'd3, C_WROTE = 4'd4, C_READW = 4'd5,
316             X_START = 4'd6, X_LEAVE = 4'd7, X_WAIT = 4'd8;
317  reg [3:0] st, rret; reg [7:0] cmd; reg [3:0] argi, argc; reg [7:0] args [0:13];
318  reg [7:0] rb [0:8]; reg [3:0] ri, rlen; reg [3:0] lcnt; reg x_on;
319  wire [15:0] arg16_0 = {args[1], args[0]};
320  wire [31:0] arg32_2 = {args[5], args[4], args[3], args[2]};
321  wire [63:0] arg64_6 = {args[13], args[12], args[11], args[10], args[9], args[8], args[7], args[6]};
322  function [3:0] nargs(input [7:0] c);
323    case (c) 8'h57: nargs = 4'd14; 8'h52: nargs = 4'd6; default: nargs = 4'd0; endcase
324  endfunction
325  assign led = {x_on, f_halted, st[3:0], tq_n != 8'd0, f_done};
326  always @(posedge clk) begin
327    q_push <= 1'b0; h_we <= 1'b0; f_rst <= 1'b0; f_start <= 1'b0; f_go <= 1'b0;
328    if (rst) begin st <= C_IDLE; h_re <= 1'b0; x_on <= 1'b0; end
329    else case (st)
330      C_IDLE: if (rx_v) begin
331        cmd <= rx_b; argi <= 4'd0; argc <= nargs(rx_b);
332        st <= (nargs(rx_b) == 4'd0) ? C_EXEC : C_ARGS;
333      end
334      C_ARGS: if (rx_v) begin
335        args[argi] <= rx_b; argi <= argi + 4'd1;
336        if (argi + 4'd1 == argc) st <= C_EXEC;
337      end
338      C_EXEC: case (cmd)
339        8'h49: begin rb[0] <= 8'h69; rb[1] <= 8'h4c; rb[2] <= 8'h4f; rb[3] <= 8'h47; rb[4] <= 8'h53; rb[5] <= LAT;
340                     rlen <= 4'd6; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY; end
341        8'h57: begin h_we <= 1'b1; h_tensor <= arg16_0; h_addr <= arg32_2; h_wdata <= arg64_6; st <= C_WROTE; end
342        8'h52: begin h_re <= 1'b1; h_tensor <= arg16_0; h_addr <= arg32_2; lcnt <= LAT; st <= C_READW; end
343        // A step starts with a reset and a start; a halted step continues past its halt and leaves it.
344        8'h58: if (!x_on) begin f_rst <= 1'b1; st <= X_START; end else begin f_go <= 1'b1; st <= X_LEAVE; end
345        default: begin rb[0] <= 8'h3f; rlen <= 4'd1; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY; end
346      endcase
347      C_REPLY: if (ri == rlen) st <= rret;
348               else if (q_room && !q_push) begin q_push <= 1'b1; q_byte <= rb[ri]; ri <= ri + 4'd1; end
349      C_WROTE: begin rb[0] <= 8'h6b; rlen <= 4'd1; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY; end
350      C_READW: if (lcnt == 4'd0) begin
351        h_re <= 1'b0; rb[0] <= 8'h72;
352        rb[1] <= h_rdata[7:0]; rb[2] <= h_rdata[15:8]; rb[3] <= h_rdata[23:16]; rb[4] <= h_rdata[31:24];
353        rb[5] <= h_rdata[39:32]; rb[6] <= h_rdata[47:40]; rb[7] <= h_rdata[55:48]; rb[8] <= h_rdata[63:56];
354        rlen <= 4'd9; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY;
355      end else lcnt <= lcnt - 4'd1;
356      X_START: begin f_start <= 1'b1; x_on <= 1'b1; st <= X_WAIT; end
357      X_LEAVE: if (!f_halted) st <= X_WAIT;
358      // Halted again, or finished: the link (0xFFFF at the end) and the cycles run.
359      X_WAIT: if (f_done && !f_start) begin
360        x_on <= 1'b0; rb[0] <= 8'h78; rb[1] <= 8'hff; rb[2] <= 8'hff;
361        rb[3] <= f_cycles[7:0]; rb[4] <= f_cycles[15:8]; rb[5] <= f_cycles[23:16]; rb[6] <= f_cycles[31:24];
362        rlen <= 4'd7; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY;
363      end else if (f_halted) begin
364        rb[0] <= 8'h78; rb[1] <= f_halt_at[7:0]; rb[2] <= f_halt_at[15:8];
365        rb[3] <= f_cycles[7:0]; rb[4] <= f_cycles[15:8]; rb[5] <= f_cycles[23:16]; rb[6] <= f_cycles[31:24];
366        rlen <= 4'd7; ri <= 4'd0; rret <= C_IDLE; st <= C_REPLY;
367      end
368      default: st <= C_IDLE;
369    endcase
370  end
371endmodule
372",
373    );
374    s
375}
376
377/// What a host needs of one chip's program of a program split over chips, as its ledger states it: the twin's count at
378/// each halt and at the end ([`fabric_cycle_segments`](super::fabric::fabric_cycle_segments)), each link — the section it
379/// follows, whether the fabric halts there, and each tensor it sends and receives with its id and every element's host
380/// word, row-major — the words of the step's inputs (`tok`, `pos`) the chip holds, and of its result (`next`) if it holds
381/// it. `None` where the fabric does not lower `p`.
382pub fn chip_record(p: &SpatialProgram, target: &crate::spatial::target::SpatialTarget) -> Option<serde_json::Value> {
383    let segments = super::fabric::fabric_cycle_segments(p, target)?;
384    let words = |name: &str| -> Option<serde_json::Value> {
385        let t = p.tensors.iter().find(|t| t.name == name)?;
386        let w: Vec<usize> = (0..t.elems.rows * t.elems.cols).map(|i| host_word(p, t, i / t.elems.cols, i % t.elems.cols)).collect();
387        Some(serde_json::json!({ "name": name, "tensor": t.id.idx(), "words": w }))
388    };
389    let named = |ids: &[crate::spatial::ir::TensorId]| -> Vec<serde_json::Value> {
390        ids.iter().filter_map(|t| words(&p.tensor(*t).name)).collect()
391    };
392    let links: Vec<serde_json::Value> = p
393        .links
394        .iter()
395        .map(|l| {
396            serde_json::json!({
397                "after": l.after,
398                "halts": l.after.is_some_and(|k| k + 1 < p.sections.len()),
399                "sends": named(&l.sends),
400                "receives": named(&l.receives),
401            })
402        })
403        .collect();
404    let inputs: Vec<serde_json::Value> = ["tok", "pos"].iter().filter_map(|n| words(n)).collect();
405    Some(serde_json::json!({
406        "segments": segments,
407        "links": links,
408        "inputs": inputs,
409        "next": words("next").unwrap_or(serde_json::Value::Null),
410    }))
411}
412
413/// The relay as a split ledger states it: `{"run": chip, "to": link}` and `{"copy": tensor, "from": chip, "to": chip}`.
414pub fn relay_record(relay: &[crate::spatial::tensor_parallel::RelayStep]) -> serde_json::Value {
415    use crate::spatial::tensor_parallel::RelayStep;
416    serde_json::Value::Array(
417        relay
418            .iter()
419            .map(|s| match s {
420                RelayStep::Run { chip, to } => serde_json::json!({ "run": chip, "to": to }),
421                RelayStep::Copy { tensor, from, to } => serde_json::json!({ "copy": tensor, "from": from, "to": to }),
422            })
423            .collect(),
424    )
425}
426
427/// The protocol a board image serves over its byte channel: the decode step's (`decode_board`: `I`, `W`, `R`, `S`, `G`)
428/// or a chip's of a program split over chips (`chip_board`: `I`, `W`, `R`, `X`).
429#[derive(Clone, Copy, Debug, PartialEq, Eq)]
430pub enum BoardProtocol {
431    Decode(DecodeWords),
432    Chip,
433}
434
435impl BoardProtocol {
436    /// The protocol's module, and the name of its file in an image.
437    pub fn module(self) -> &'static str {
438        match self {
439            BoardProtocol::Decode(_) => "decode_board",
440            BoardProtocol::Chip => "chip_board",
441        }
442    }
443
444    /// The protocol's module, its host reads `lat` cycles long.
445    pub fn verilog(self, lat: u64) -> String {
446        match self {
447            BoardProtocol::Decode(w) => decode_board_verilog(w, lat),
448            BoardProtocol::Chip => chip_board_verilog(lat),
449        }
450    }
451
452    /// What an image serving it is, as its constraints say.
453    fn what(self) -> &'static str {
454        match self {
455            BoardProtocol::Decode(_) => "the decode step's board image",
456            BoardProtocol::Chip => "a chip's board image of a program split over chips",
457        }
458    }
459}
460
461/// A Nexys board the image is built for.
462#[derive(Clone, Copy, Debug, PartialEq, Eq)]
463pub enum NexysBoard {
464    /// The Nexys A7-100T (XC7A100T-1CSG324C): its byte channel is the FT2232's UART bridge.
465    A7,
466    /// The Nexys Video (XC7A200T-1SBG484C): its byte channel is the FT2232's FT245 FIFO, which the FT245 bridge
467    /// ([`FT245_UART_BRIDGE_V`]) turns into the serial line `decode_board` speaks.
468    Video,
469}
470
471/// The FT245 bridge a Nexys Video top instantiates: the board's FT245 FIFO to a serial pair.
472/// `benchmarks/silicon/ft245_uart_bridge.v` is its golden twin, for the board flows that read it there.
473pub const FT245_UART_BRIDGE_V: &str = include_str!("../../codegen_sva/ft245_uart_bridge.v");
474
475/// The serial line between the FT245 bridge and `decode_board` on the Nexys Video, in core clocks per bit: on chip, so
476/// short; the host's side runs at the FIFO's own rate.
477const BRIDGE_CPB: u32 = 8;
478
479impl NexysBoard {
480    /// The part, as the open flow names it.
481    pub fn part(self) -> &'static str {
482        match self {
483            NexysBoard::A7 => "xc7a100tcsg324-1",
484            NexysBoard::Video => "xc7a200tsbg484-1",
485        }
486    }
487
488    /// The hand-written Verilog the top instantiates beside the step and its protocol, each by its file name and text.
489    pub fn extra_sources(self) -> &'static [(&'static str, &'static str)] {
490        match self {
491            NexysBoard::A7 => &[],
492            NexysBoard::Video => &[("ft245_uart_bridge.v", FT245_UART_BRIDGE_V)],
493        }
494    }
495
496    /// The board's top `top`: the 100 MHz oscillator through a PLL onto a global clock buffer for the core at `clock`,
497    /// a reset held until the PLL locks and eight cycles after, `protocol`'s module on the board's byte channel, and the
498    /// eight status LEDs.
499    pub fn top_verilog(self, top: &str, clock: CoreClock, protocol: BoardProtocol) -> String {
500        let module = protocol.module();
501        let core = format!(
502            "  wire clkfb, core_raw, locked, coreclk;\n\
503             \x20 PLLE2_BASE #(.CLKIN1_PERIOD(10.0), .CLKFBOUT_MULT({}), .DIVCLK_DIVIDE({}), .CLKOUT0_DIVIDE({})) pll (\n\
504             \x20   .CLKIN1(clk), .CLKFBIN(clkfb), .CLKFBOUT(clkfb), .CLKOUT0(core_raw), .LOCKED(locked), .RST(1'b0), .PWRDWN(1'b0));\n\
505             \x20 BUFG bufg_core (.I(core_raw), .O(coreclk));\n",
506            clock.mult, clock.div, clock.out
507        );
508        let reset = "  reg [3:0] por = 4'd0;\n  always @(posedge coreclk) if (!locked) por <= 4'd0; else if (!por[3]) por <= por + 4'd1;\n";
509        match self {
510            NexysBoard::A7 => format!(
511                "module {top} (input clk, input uart_rx, output uart_tx, output [7:0] led);\n{core}{reset}\
512                 \x20 {module} #(.CLKS_PER_BIT({cpb})) board(.clk(coreclk), .rst(!por[3]), .uart_rx(uart_rx), .uart_tx(uart_tx), .led(led));\n\
513                 endmodule\n",
514                cpb = clks_per_bit(clock)
515            ),
516            NexysBoard::Video => format!(
517                "module {top} (input clk, inout [7:0] prog_d, input prog_rxen, input prog_txen, output prog_rdn, output prog_wrn,\n\
518                 \x20 output prog_oen, output prog_siwun, output prog_spien, output [7:0] led);\n{core}{reset}\
519                 \x20 wire to_board, from_board;\n\
520                 \x20 ft245_uart_bridge #(.CLKS_PER_BIT({BRIDGE_CPB}), .W(4)) bridge(.clk(coreclk), .rst(!por[3]), .eng_rx(to_board), .eng_tx(from_board),\n\
521                 \x20   .prog_d(prog_d), .prog_rxen(prog_rxen), .prog_txen(prog_txen), .prog_rdn(prog_rdn), .prog_wrn(prog_wrn), .prog_oen(prog_oen),\n\
522                 \x20   .prog_siwun(prog_siwun), .prog_spien(prog_spien));\n\
523                 \x20 {module} #(.CLKS_PER_BIT({BRIDGE_CPB})) board(.clk(coreclk), .rst(!por[3]), .uart_rx(to_board), .uart_tx(from_board), .led(led));\n\
524                 endmodule\n"
525            ),
526        }
527    }
528
529    /// The constraints for `top`: the oscillator, the byte channel's pins and the LEDs, from Digilent's master XDC
530    /// files.
531    pub fn xdc(self, top: &str, protocol: BoardProtocol) -> String {
532        let pin = |p: &str, port: &str, io: &str| format!("set_property -dict {{ PACKAGE_PIN {p} IOSTANDARD {io} }} [get_ports {{ {port} }}]\n");
533        let mut s = format!("# {top}: {} on the {self:?} ({}).\n", protocol.what(), self.part());
534        let (clk, leds, io_led): (&str, [&str; 8], &str) = match self {
535            NexysBoard::A7 => ("E3", ["H17", "K15", "J13", "N14", "R18", "V17", "U17", "U16"], "LVCMOS33"),
536            NexysBoard::Video => ("R4", ["T14", "T15", "T16", "U16", "V15", "W16", "W15", "Y13"], "LVCMOS25"),
537        };
538        s.push_str(&pin(clk, "clk", "LVCMOS33"));
539        s.push_str("create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports { clk }]\n");
540        match self {
541            NexysBoard::A7 => {
542                s.push_str(&pin("C4", "uart_rx", "LVCMOS33"));
543                s.push_str(&pin("D4", "uart_tx", "LVCMOS33"));
544            }
545            NexysBoard::Video => {
546                for (i, p) in ["U20", "P14", "P15", "U17", "R17", "P16", "R18", "N14"].iter().enumerate() {
547                    s.push_str(&pin(p, &format!("prog_d[{i}]"), "LVCMOS33"));
548                }
549                for (p, port) in [("N17", "prog_rxen"), ("Y19", "prog_txen"), ("P19", "prog_rdn"), ("R19", "prog_wrn"), ("V17", "prog_oen"), ("P17", "prog_siwun"), ("R14", "prog_spien")] {
550                    s.push_str(&pin(p, port, "LVCMOS33"));
551                }
552            }
553        }
554        for (i, p) in leds.iter().enumerate() {
555            s.push_str(&pin(p, &format!("led[{i}]"), io_led));
556        }
557        s
558    }
559}
560
561/// Every file a board build reads, by name: the program's RTL and the memory images its `$readmemh` loads, `protocol`'s
562/// module with the serial pair, the board's top — named `top`, its core at `clock` — its constraints, and the
563/// hand-written Verilog the top instantiates ([`NexysBoard::extra_sources`]).
564pub fn board_image(
565    art: &VerilogArtifact,
566    images: &[(String, String)],
567    protocol: BoardProtocol,
568    lat: u64,
569    board: NexysBoard,
570    top: &str,
571    clock: CoreClock,
572) -> Vec<(String, String)> {
573    let mut files = vec![
574        ("spatial_top.v".to_string(), art.rtl.clone()),
575        (format!("{}.v", protocol.module()), protocol.verilog(lat)),
576        (format!("{top}.v"), board.top_verilog(top, clock, protocol)),
577        (format!("{top}.xdc"), board.xdc(top, protocol)),
578    ];
579    files.extend(art.hex.iter().cloned());
580    // The program's own fills overwrite the artifact's placeholder images of the same name.
581    for (name, body) in images {
582        match files.iter_mut().find(|(n, _)| n == name) {
583            Some(slot) => slot.1 = body.clone(),
584            None => files.push((name.clone(), body.clone())),
585        }
586    }
587    files.extend(board.extra_sources().iter().map(|(name, text)| (name.to_string(), text.to_string())));
588    files
589}
590
591/// What a board's memories hold when it is configured: each tensor's fill, and zero for one supplied at run time
592/// ([`TensorInit::External`], which the host or the link writes before the program reads it).
593fn configured_memories(p: &SpatialProgram) -> Result<crate::spatial::eval::Inputs, BoardImageError> {
594    use crate::spatial::eval::{Inputs, Value};
595    use crate::spatial::ir::{TensorInit, TensorRole};
596    let mut inputs = Inputs::default();
597    for t in p.tensors.iter().filter(|t| t.role != TensorRole::Output) {
598        let n = t.elems.rows * t.elems.cols;
599        let value = if t.init == TensorInit::External {
600            Some(Value::Int(vec![0; n]))
601        } else if t.init.is_float() {
602            (0..n).map(|i| t.init.element_f64(i)).collect::<Option<Vec<f64>>>().map(Value::Float)
603        } else {
604            (0..n).map(|i| t.init.element(i)).collect::<Option<Vec<i64>>>().map(Value::Int)
605        };
606        let value = value.ok_or_else(|| BoardImageError::Fill(format!("`{}` has no deterministic fill", t.name)))?;
607        inputs.tensors.insert(t.id, value);
608    }
609    Ok(inputs)
610}
611
612/// Why a program has no board image.
613#[derive(Clone, Debug, PartialEq)]
614pub enum BoardImageError {
615    /// The fabric does not lower it.
616    Lower(SpatialLowerError),
617    /// A tensor it reads has no deterministic fill to load its memory with, and is not supplied at run time.
618    Fill(String),
619    /// The target does not say how many cycles its memories take to read.
620    ReadLatency,
621}
622
623/// What a board image runs: a whole decode step, which the host steps and the chip loops ([`BoardProtocol::Decode`]),
624/// or one chip's program of a split, which the host runs from halt to halt ([`BoardProtocol::Chip`]).
625#[derive(Clone, Copy, Debug, PartialEq, Eq)]
626pub enum ImageRole {
627    DecodeStep,
628    Chip,
629}
630
631/// The board image of `p`, a fabric program for `target`: lowered, its memories loaded with its fills, on `board` with
632/// the top named `top` and its core at `clock`, speaking `role`'s protocol (a decode step's through its `tok`, `pos` and
633/// `next` words). Every file synthesis reads, so its [`image_digest`] is the image's identity.
634pub fn program_board_image(
635    p: &SpatialProgram,
636    target: &crate::spatial::target::SpatialTarget,
637    board: NexysBoard,
638    role: ImageRole,
639    top: &str,
640    clock: CoreClock,
641) -> Result<Vec<(String, String)>, BoardImageError> {
642    use crate::spatial::lower::SpatialBackend;
643    let inputs = configured_memories(p)?;
644    let art = match crate::spatial::lower::fabric::FabricBackend::new(target.clone()).lower(p) {
645        Ok(crate::spatial::lower::SpatialArtifact::Verilog(v)) => v,
646        Ok(_) => unreachable!("the fabric backend lowers to Verilog"),
647        Err(e) => return Err(BoardImageError::Lower(e)),
648    };
649    let lat = target.site.local_mem_read_latency.ok_or(BoardImageError::ReadLatency)? as u64;
650    let protocol = match role {
651        ImageRole::DecodeStep => BoardProtocol::Decode(DecodeWords::of(p, "tok", "pos", "next").map_err(BoardImageError::Lower)?),
652        ImageRole::Chip => BoardProtocol::Chip,
653    };
654    let images = crate::spatial::lower::fabric::input_hex_files(p, &inputs);
655    Ok(board_image(&art, &images, protocol, lat, board, top, clock))
656}
657
658/// The identity of a board build: the sha256, in hex, of every file synthesis reads — in name order, each its name, a
659/// zero byte, its text and a zero byte — with every Verilog file's simulation-only regions (`synthesis translate_off`
660/// through `synthesis translate_on`) taken out, so a check only a simulation runs moves no bitstream's identity.
661pub fn image_digest(files: &[(String, String)]) -> String {
662    let mut sorted: Vec<&(String, String)> = files.iter().collect();
663    sorted.sort_by(|a, b| a.0.cmp(&b.0));
664    let mut msg = Vec::new();
665    for (name, body) in sorted {
666        msg.extend_from_slice(name.as_bytes());
667        msg.push(0);
668        let text = if name.ends_with(".v") { synthesized(body) } else { body.clone() };
669        msg.extend_from_slice(text.as_bytes());
670        msg.push(0);
671    }
672    crate::crypto_accel::sha256(&msg).iter().map(|b| format!("{b:02x}")).collect()
673}
674
675/// `verilog` as synthesis reads it: without its simulation-only regions.
676pub fn synthesized(verilog: &str) -> String {
677    let mut out = String::new();
678    let mut simulation_only = false;
679    for line in verilog.lines() {
680        if line.contains("synthesis translate_off") {
681            simulation_only = true;
682        } else if line.contains("synthesis translate_on") {
683            simulation_only = false;
684        } else if !simulation_only {
685            out.push_str(line);
686            out.push('\n');
687        }
688    }
689    out
690}