Skip to main content

logicaffeine_compile/codegen_sva/
terminal_protocol.rs

1//! The Fabric Terminal's wire protocol and screen, in one place: the link between a host and the terminal board
2//! (Board B, the Nexys A7-100T) that puts a LOGOS editor on a VGA screen, takes a keyboard on the board's USB port,
3//! and runs each program on the fabric engine beside them.
4//!
5//! The link is one UART, its bytes framed by SLIP (RFC 1055): a frame ends with [`SLIP_END`], and an `END` or
6//! [`SLIP_ESC`] inside a frame travels as `ESC` + [`SLIP_ESC_END`] / [`SLIP_ESC_ESC`]. A receiver that starts
7//! mid-frame, or loses a byte, is back in step at the next `END`. A frame's first byte is its kind:
8//! [`host`] kinds travel host → board, [`board`] kinds board → host.
9//!
10//! The screen is text: [`COLS`] × [`ROWS`] cells of 8 × 16 pixels on 640 × 480 at 60 Hz. A cell is 16 bits —
11//! glyph (7 bits), blink, foreground and background from the 16-colour [`PALETTE`] — addressed `row << 7 | col`,
12//! so no multiply sits in the pixel path. The bottom [`PANEL_ROWS`] rows belong to the activity panel the board
13//! draws itself from the engine's probes; text written there is covered.
14//!
15//! Every byte and dimension the RTL uses is filled into its templates from here by [`fill_verilog`], and a host
16//! that is not Rust reads [`table_json`] (`fabric_terminal --table`). Nothing restates a value.
17
18/// SLIP frame delimiter.
19pub const SLIP_END: u8 = 0xC0;
20/// SLIP escape.
21pub const SLIP_ESC: u8 = 0xDB;
22/// An `END` inside a frame, after `ESC`.
23pub const SLIP_ESC_END: u8 = 0xDC;
24/// An `ESC` inside a frame, after `ESC`.
25pub const SLIP_ESC_ESC: u8 = 0xDD;
26
27/// Frame kinds a host sends.
28pub mod host {
29    /// `row, col, cell_hi, cell_lo, …` — write consecutive cells from (row, col); each cell is two bytes, the
30    /// attribute byte (background << 4 | foreground) then the glyph byte (blink << 7 | glyph).
31    pub const PUT: u8 = 0x01;
32    /// `row, col, height, width, attribute, glyph` — fill a rectangle with one cell.
33    pub const FILL: u8 = 0x02;
34    /// `row, col, shown` — place the text cursor; `shown` 0 hides it.
35    pub const CURSOR: u8 = 0x03;
36    /// `row, col, shown` — place the pointer (a mouse's position, drawn as an inverted cell).
37    pub const POINTER: u8 = 0x04;
38    /// `row, col, rows, cols` — the small-text window: text rows `row..row + rows` and columns `col..col + cols`
39    /// show the small plane ([`super::SMALL_FIRST_ROW`]) instead, two of its 8-pixel rows to each text row, drawn
40    /// in the small font ([`super::FONT8_ROWS`]); `rows` 0 closes it.
41    pub const WINDOW: u8 = 0x05;
42    /// `byte, …` — bytes for the fabric engine's loader, in order; at most [`super::ENGINE_FRAME_MAX`] per frame,
43    /// and the next frame only after [`super::board::ENGINE_TAKEN`].
44    pub const ENGINE: u8 = 0x10;
45    /// No payload — hold the fabric engine in reset for one engine clock and release it: a running program
46    /// stops, and the loader waits for a new image.
47    pub const ENGINE_RESET: u8 = 0x11;
48    /// `b3, b2, b1, b0` — a 32-bit value for the 7-segment display, shown in signed decimal.
49    pub const RESULT: u8 = 0x12;
50    /// No payload — answered by [`super::board::ACTIVITY`], the fabric engine's last frame of activity.
51    pub const ACTIVITY: u8 = 0x13;
52    /// No payload — answered by [`super::board::TOTALS`], the engine clocks of the run so far, by block and cell.
53    pub const TOTALS: u8 = 0x14;
54    /// `byte` — send one byte to the PS/2 device (a mouse needs `0xF4` before it reports).
55    pub const PS2_SEND: u8 = 0x20;
56    /// `row, col, count` — read `count` cells back from (row, col); answered by [`super::board::CELLS`].
57    pub const READ: u8 = 0x30;
58    /// No payload — answered by [`super::board::PONG`].
59    pub const PING: u8 = 0x3F;
60    pub const ALL: [(&str, u8); 13] = [
61        ("put", PUT),
62        ("fill", FILL),
63        ("cursor", CURSOR),
64        ("pointer", POINTER),
65        ("window", WINDOW),
66        ("engine", ENGINE),
67        ("engine_reset", ENGINE_RESET),
68        ("result", RESULT),
69        ("activity", ACTIVITY),
70        ("totals", TOTALS),
71        ("ps2_send", PS2_SEND),
72        ("read", READ),
73        ("ping", PING),
74    ];
75}
76
77/// Frame kinds a board sends.
78pub mod board {
79    /// `byte, status` — one byte from the PS/2 device; `status` is 0, or [`super::PS2_PARITY_ERROR`] /
80    /// [`super::PS2_FRAMING_ERROR`] for a byte received damaged.
81    pub const PS2: u8 = 0x81;
82    /// `status` — the outcome of a [`super::host::PS2_SEND`]: 0 acknowledged by the device, else
83    /// [`super::PS2_NO_ACK`].
84    pub const PS2_SENT: u8 = 0x82;
85    /// `byte, …` — bytes the fabric engine sent (its Show records and result frame, `chip_protocol`'s format).
86    pub const ENGINE: u8 = 0x90;
87    /// `count` — the `count` bytes of the last [`super::host::ENGINE`] frame are inside the engine; the host sends
88    /// the next. The engine takes bytes slower than the link brings them, so a host waits for this between frames.
89    pub const ENGINE_TAKEN: u8 = 0x91;
90    /// [`super::ACTIVITY_FIELDS`] — the fabric engine in the last whole video frame, as the activity panel drew it:
91    /// the frame's number, whether a program ran, its block, cell, frame depth, heap frontier and run cycles at the
92    /// frame's snapshot, and how many engine clocks it spent in each block and in each cell during the frame. The
93    /// frame's number comes again last: a reply whose two numbers differ spans a frame's turnover and is not one
94    /// frame's counts.
95    pub const ACTIVITY: u8 = 0x92;
96    /// [`super::TOTALS_FIELDS`] — the run's cycles as the activity panel's last snapshot holds them, then the engine
97    /// clocks the run spent in each block and in each cell since the engine was last reset, every one counted, each
98    /// total holding at its largest rather than wrapping. Once the run has ended and a frame has passed, they are its
99    /// whole profile: its block totals, and its cell totals, each add up to its cycles and one (the clock that sees
100    /// it done). Read during a run, each total is as it stood when its bytes were sent.
101    pub const TOTALS: u8 = 0x93;
102    /// `row, col, (cell_hi, cell_lo) × count` — cells read back for a [`super::host::READ`].
103    pub const CELLS: u8 = 0xB0;
104    /// [`super::PONG_FIELDS`] — the board is up, the screen it draws, what the image holds (a bit per feature,
105    /// [`super::FEATURE_ENGINE`], [`super::FEATURE_SMALL`], [`super::FEATURE_TOTALS`]), and the PS/2 line since the
106    /// image started: the clock edges the device drove, the shortest clock-low phase among them in microseconds
107    /// (255 until one is shorter), and the bytes read whole and damaged.
108    pub const PONG: u8 = 0xBF;
109    pub const ALL: [(&str, u8); 8] = [
110        ("ps2", PS2),
111        ("ps2_sent", PS2_SENT),
112        ("engine", ENGINE),
113        ("engine_taken", ENGINE_TAKEN),
114        ("activity", ACTIVITY),
115        ("totals", TOTALS),
116        ("cells", CELLS),
117        ("pong", PONG),
118    ];
119}
120
121/// [`board::PONG`]'s payload, a byte each in this order; a two-byte count is high byte first.
122pub const PONG_FIELDS: [&str; 11] = [
123    "version",
124    "cols",
125    "rows",
126    "panel_rows",
127    "features",
128    "ps2_edges_hi",
129    "ps2_edges_lo",
130    "ps2_shortest_low_us",
131    "ps2_bytes_hi",
132    "ps2_bytes_lo",
133    "ps2_damaged",
134];
135
136/// [`board::ACTIVITY`]'s payload, field by field: (name, bytes each, how many), every count big-endian. The per-block
137/// and per-cell clocks are one count per block slot and per cell slot of Board B's engine.
138pub const ACTIVITY_FIELDS: [(&str, usize, usize); 10] = [
139    ("frame", 2, 1),
140    ("running", 1, 1),
141    ("block", 2, 1),
142    ("cell", 2, 1),
143    ("depth", 2, 1),
144    ("heap", 2, 1),
145    ("cycles", 4, 1),
146    ("block_clocks", 2, super::wprog_engine_rtl::WPE_HW_MAXBLK),
147    ("cell_clocks", 2, super::wprog_engine_rtl::WPE_HW_MAXCELL),
148    ("frame_again", 2, 1),
149];
150
151/// [`board::TOTALS`]'s payload, field by field, as [`ACTIVITY_FIELDS`] is laid out.
152pub const TOTALS_FIELDS: [(&str, usize, usize); 3] = [
153    ("cycles", 4, 1),
154    ("block_totals", 4, super::wprog_engine_rtl::WPE_HW_MAXBLK),
155    ("cell_totals", 4, super::wprog_engine_rtl::WPE_HW_MAXCELL),
156];
157
158const fn payload_bytes(fields: &[(&str, usize, usize)]) -> usize {
159    let mut n = 0;
160    let mut i = 0;
161    while i < fields.len() {
162        n += fields[i].1 * fields[i].2;
163        i += 1;
164    }
165    n
166}
167
168/// The length of [`board::ACTIVITY`]'s payload.
169pub const fn activity_bytes() -> usize {
170    payload_bytes(&ACTIVITY_FIELDS)
171}
172
173/// The length of [`board::TOTALS`]'s payload.
174pub const fn totals_bytes() -> usize {
175    payload_bytes(&TOTALS_FIELDS)
176}
177
178/// The protocol's version, as [`board::PONG`] reports it.
179pub const VERSION: u8 = 1;
180
181/// [`board::PONG`]'s feature bit for an image with the fabric engine: ENGINE frames reach an engine that answers.
182/// Without it (the screen-and-keyboard shell) ENGINE bytes are taken and dropped.
183pub const FEATURE_ENGINE: u8 = 0x01;
184
185/// [`board::PONG`]'s feature bit for an image with the small-text plane and [`host::WINDOW`].
186pub const FEATURE_SMALL: u8 = 0x02;
187
188/// [`board::PONG`]'s feature bit for an image that keeps the run's totals: [`host::TOTALS`] is answered.
189pub const FEATURE_TOTALS: u8 = 0x04;
190
191/// The small-text plane: text-buffer rows `SMALL_FIRST_ROW..SMALL_FIRST_ROW + SMALL_ROWS`, of which columns
192/// `0..SMALL_COLS` hold cells. PUT, FILL and READ address them like any row; [`host::WINDOW`] puts them on screen.
193pub const SMALL_FIRST_ROW: u8 = 32;
194pub const SMALL_ROWS: u8 = 32;
195pub const SMALL_COLS: u8 = 32;
196
197/// The small font: glyph `g`'s row `r` is row `FONT8_ROWS[r]` of `g` in the 8×16 font — the rows that keep its
198/// round letters round, its digits whole and its box-drawing lines joined from one small row to the next.
199pub const FONT8_ROWS: [usize; 8] = [2, 4, 5, 7, 8, 10, 11, 13];
200
201/// The most engine bytes one [`host::ENGINE`] frame carries: the board's engine FIFO holds one frame.
202pub const ENGINE_FRAME_MAX: usize = 255;
203
204/// The fabric engine's clock divider from the board clock, and its UART's clocks per bit: the board feeds the
205/// engine's loader through an internal UART at `BOARD_HZ / ENGINE_CLOCK_DIVIDER / ENGINE_CLKS_PER_BIT` baud.
206pub const ENGINE_CLOCK_DIVIDER: u32 = 128;
207pub const ENGINE_CLKS_PER_BIT: u32 = 8;
208
209/// A PS/2 byte whose odd parity did not hold.
210pub const PS2_PARITY_ERROR: u8 = 0x01;
211/// A PS/2 frame whose start bit was not 0 or stop bit not 1.
212pub const PS2_FRAMING_ERROR: u8 = 0x02;
213/// A host-to-device byte the device never acknowledged.
214pub const PS2_NO_ACK: u8 = 0x01;
215
216/// Text columns.
217pub const COLS: u8 = 80;
218/// Text rows.
219pub const ROWS: u8 = 30;
220/// Rows at the bottom that the activity panel draws over.
221pub const PANEL_ROWS: u8 = 4;
222/// Glyphs in the font ROM: a cell's glyph field is 7 bits.
223pub const GLYPHS: usize = 128;
224/// Glyph height in pixels (width is 8).
225pub const GLYPH_ROWS: usize = 16;
226
227/// The characters the font holds below the printable range, slot `i` holding character `i`: light and double box
228/// lines, blocks and shades, arrows, markers and a tick — what a terminal UI draws with. Slots 0x20–0x7E are ASCII
229/// and slot 0x7F holds [`DEL_GLYPH`]. A host maps a Unicode character to its slot by this table, and
230/// `scripts/dev/gen-terminal-font.py` builds the font ROM by it.
231pub const UI_GLYPHS: &str = "─│┌┐└┘├┤┬┴┼═║╔╗╚╝█▓▒░▀▄▌▐▶◀▲▼•·√";
232/// The character slot 0x7F holds.
233pub const DEL_GLYPH: char = '■';
234
235/// The font slot that draws `ch`: ASCII as itself, [`UI_GLYPHS`] and [`DEL_GLYPH`] by their slots, anything else
236/// as `?`.
237pub fn glyph_slot(ch: char) -> u8 {
238    match ch {
239        ' '..='~' => ch as u8,
240        _ if ch == DEL_GLYPH => 0x7F,
241        _ => UI_GLYPHS.chars().position(|c| c == ch).map_or(b'?', |i| i as u8),
242    }
243}
244
245/// The link's baud rate: 100 clocks per bit at the board's 100 MHz, and FTDI's 3 MHz base divided by 3, so
246/// both ends run it exactly.
247pub const BAUD: u32 = 1_000_000;
248/// The board clock in Hz.
249pub const BOARD_HZ: u32 = 100_000_000;
250
251/// 640 × 480 at 60 Hz: (visible, front porch, sync, back porch) per axis in pixels / lines, sync active low,
252/// one pixel per 4 board clocks (25 MHz; monitors lock to it within the standard's 0.7% of 25.175 MHz).
253pub const H_TIMING: (u16, u16, u16, u16) = (640, 16, 96, 48);
254pub const V_TIMING: (u16, u16, u16, u16) = (480, 10, 2, 33);
255pub const CLOCKS_PER_PIXEL: u32 = 4;
256
257/// The 16 colours a cell can name, as the board's 4-bit-per-channel DAC drives them (0xRGB), in CGA order.
258pub const PALETTE: [u16; 16] = [
259    0x000, 0x00A, 0x0A0, 0x0AA, 0xA00, 0xA0A, 0xA50, 0xAAA, 0x555, 0x55F, 0x5F5, 0x5FF, 0xF55, 0xF5F, 0xFF5, 0xFFF,
260];
261
262/// The activity panel, drawn by the board from the fabric engine's probes over the bottom [`PANEL_ROWS`] rows. The
263/// host writes the panel's text (title, labels) as ordinary cells; the board paints its graphics over the cells
264/// named here, every frame, with nothing from the host:
265/// - [`PANEL_TITLE_ROW`]: the live run-cycle count, 10 decimal digits from column [`PANEL_CYCLES_COL`];
266/// - [`PANEL_BLOCK_ROW`]: one tile per engine block from column [`PANEL_BLOCK_COL`], [`PANEL_BLOCK_TILE`] columns
267///   wide, coloured by how often the block ran in the last frame (the [`HEAT`] ramp, log scale), the block running
268///   now outlined; the frame-stack gauge from [`PANEL_STACK_COL`] (a segment per frame) and the heap gauge from
269///   [`PANEL_HEAP_COL`] (a pixel per heap word);
270/// - [`PANEL_CELL_ROW`]: one square per program cell from column [`PANEL_CELL_COL`], two columns apart, coloured
271///   the same way, the cell being walked now outlined.
272/// The labels go on [`PANEL_LABEL_ROW`]. Tile and square counts are the engine's caps (`WPE_HW_MAXBLK`,
273/// `WPE_HW_MAXCELL`, `WPE_HW_DEPTH`, `WPE_HW_HEAP`).
274pub const PANEL_TITLE_ROW: u8 = ROWS - PANEL_ROWS;
275pub const PANEL_LABEL_ROW: u8 = PANEL_TITLE_ROW + 1;
276pub const PANEL_BLOCK_ROW: u8 = PANEL_TITLE_ROW + 2;
277pub const PANEL_CELL_ROW: u8 = PANEL_TITLE_ROW + 3;
278pub const PANEL_CYCLES_COL: u8 = 62;
279pub const PANEL_BLOCK_COL: u8 = 8;
280pub const PANEL_BLOCK_TILE: u8 = 6;
281pub const PANEL_CELL_COL: u8 = 8;
282pub const PANEL_STACK_COL: u8 = 58;
283pub const PANEL_HEAP_COL: u8 = 69;
284
285/// The heat ramp a tile or square is painted with, by level: level `k` means the block or cell ran between 2^(k-1)
286/// and 2^k - 1 engine cycles in the last frame (level 0: not at all) — a frame holds at most
287/// [`panel_frame_samples`] engine cycles, so 14 is the hottest a frame reaches. Dark blue through cyan, green,
288/// yellow and orange to red.
289pub const HEAT: [u16; 16] = [
290    0x112, 0x124, 0x136, 0x148, 0x16A, 0x18B, 0x1AB, 0x2C9, 0x4D6, 0x7D4, 0xAD3, 0xCC2, 0xEA1, 0xF81, 0xF51, 0xF22,
291];
292/// The panel's other colours (0xRGB): the outline of the tile or square the engine is in, the cycle digits, and the
293/// gauges' lit and unlit segments. Everything else in a panel region is black.
294pub const PANEL_OUTLINE: u16 = 0xFFF;
295pub const PANEL_DIGIT: u16 = 0xFFF;
296pub const PANEL_STACK_LIT: u16 = 0x4D6;
297pub const PANEL_HEAP_LIT: u16 = 0x1AB;
298pub const PANEL_GAUGE_UNLIT: u16 = 0x112;
299
300/// The most engine clocks one frame of the activity panel can count: snapshots are a frame apart on the engine's
301/// clock, so a frame's counts span a frame's board clocks over [`ENGINE_CLOCK_DIVIDER`], plus the one sample a
302/// snapshot can slip by.
303pub const fn panel_frame_samples() -> u32 {
304    let ht = (H_TIMING.0 + H_TIMING.1 + H_TIMING.2 + H_TIMING.3) as u32;
305    let vt = (V_TIMING.0 + V_TIMING.1 + V_TIMING.2 + V_TIMING.3) as u32;
306    ht * vt * CLOCKS_PER_PIXEL / ENGINE_CLOCK_DIVIDER + 1
307}
308
309/// The heat level a count of engine clocks paints: its bit length.
310pub const fn heat_level(hits: u32) -> usize {
311    (32 - hits.leading_zeros()) as usize
312}
313
314/// One cell: its attribute and glyph bytes as they travel and as the text buffer holds them.
315#[derive(Clone, Copy, Debug, PartialEq, Eq)]
316pub struct Cell {
317    pub glyph: u8,
318    pub blink: bool,
319    pub fg: u8,
320    pub bg: u8,
321}
322
323impl Cell {
324    pub fn new(glyph: u8, fg: u8, bg: u8) -> Self {
325        Cell { glyph: glyph & 0x7F, blink: false, fg: fg & 0x0F, bg: bg & 0x0F }
326    }
327
328    /// The two bytes on the wire and in the buffer: attribute (bg << 4 | fg), then glyph (blink << 7 | glyph).
329    pub fn bytes(self) -> [u8; 2] {
330        [(self.bg << 4) | self.fg, ((self.blink as u8) << 7) | (self.glyph & 0x7F)]
331    }
332
333    pub fn from_bytes(attr: u8, glyph: u8) -> Self {
334        Cell { glyph: glyph & 0x7F, blink: glyph & 0x80 != 0, fg: attr & 0x0F, bg: attr >> 4 }
335    }
336}
337
338/// A text-buffer address: `row << 7 | col`.
339pub fn cell_address(row: u8, col: u8) -> u16 {
340    ((row as u16) << 7) | col as u16
341}
342
343/// One frame as SLIP puts it on the wire: the escaped bytes, then `END`.
344pub fn slip_encode(frame: &[u8]) -> Vec<u8> {
345    let mut out = Vec::with_capacity(frame.len() + 2);
346    for &b in frame {
347        match b {
348            SLIP_END => out.extend_from_slice(&[SLIP_ESC, SLIP_ESC_END]),
349            SLIP_ESC => out.extend_from_slice(&[SLIP_ESC, SLIP_ESC_ESC]),
350            _ => out.push(b),
351        }
352    }
353    out.push(SLIP_END);
354    out
355}
356
357/// A SLIP receiver: bytes in, whole frames out. An empty frame (two `END`s in a row) is no frame; an `ESC`
358/// followed by anything but `ESC_END` / `ESC_ESC` drops the frame it is in.
359#[derive(Default, Debug)]
360pub struct SlipDecoder {
361    frame: Vec<u8>,
362    escaped: bool,
363    damaged: bool,
364}
365
366impl SlipDecoder {
367    pub fn new() -> Self {
368        Self::default()
369    }
370
371    /// Feed one byte; returns a frame when this byte ended one.
372    pub fn push(&mut self, b: u8) -> Option<Vec<u8>> {
373        if b == SLIP_END {
374            let frame = std::mem::take(&mut self.frame);
375            let damaged = std::mem::replace(&mut self.damaged, false);
376            self.escaped = false;
377            return (!frame.is_empty() && !damaged).then_some(frame);
378        }
379        if self.escaped {
380            self.escaped = false;
381            match b {
382                SLIP_ESC_END => self.frame.push(SLIP_END),
383                SLIP_ESC_ESC => self.frame.push(SLIP_ESC),
384                _ => self.damaged = true,
385            }
386        } else if b == SLIP_ESC {
387            self.escaped = true;
388        } else {
389            self.frame.push(b);
390        }
391        None
392    }
393
394    /// Every whole frame in `bytes`, in order.
395    pub fn decode_all(&mut self, bytes: &[u8]) -> Vec<Vec<u8>> {
396        bytes.iter().filter_map(|&b| self.push(b)).collect()
397    }
398}
399
400/// Clocks per UART bit on the board for [`BAUD`].
401pub fn clocks_per_bit() -> u32 {
402    BOARD_HZ / BAUD
403}
404
405const VERILOG_BYTES: [(&str, u8); 33] = [
406    ("SLIP_END", SLIP_END),
407    ("SLIP_ESC", SLIP_ESC),
408    ("SLIP_ESC_END", SLIP_ESC_END),
409    ("SLIP_ESC_ESC", SLIP_ESC_ESC),
410    ("H_PUT", host::PUT),
411    ("H_FILL", host::FILL),
412    ("H_CURSOR", host::CURSOR),
413    ("H_POINTER", host::POINTER),
414    ("H_WINDOW", host::WINDOW),
415    ("H_ENGINE", host::ENGINE),
416    ("H_ENGINE_RESET", host::ENGINE_RESET),
417    ("H_RESULT", host::RESULT),
418    ("H_ACTIVITY", host::ACTIVITY),
419    ("H_TOTALS", host::TOTALS),
420    ("H_PS2_SEND", host::PS2_SEND),
421    ("H_READ", host::READ),
422    ("H_PING", host::PING),
423    ("B_PS2", board::PS2),
424    ("B_PS2_SENT", board::PS2_SENT),
425    ("B_ENGINE", board::ENGINE),
426    ("B_ENGINE_TAKEN", board::ENGINE_TAKEN),
427    ("B_ACTIVITY", board::ACTIVITY),
428    ("B_TOTALS", board::TOTALS),
429    ("B_CELLS", board::CELLS),
430    ("B_PONG", board::PONG),
431    ("VERSION", VERSION),
432    ("PS2_PARITY_ERROR", PS2_PARITY_ERROR),
433    ("PS2_FRAMING_ERROR", PS2_FRAMING_ERROR),
434    ("PS2_NO_ACK", PS2_NO_ACK),
435    ("PANEL_ROWS", PANEL_ROWS),
436    ("FEATURE_ENGINE", FEATURE_ENGINE),
437    ("FEATURE_SMALL", FEATURE_SMALL),
438    ("FEATURE_TOTALS", FEATURE_TOTALS),
439];
440
441const VERILOG_COLOURS: [(&str, u16); 5] = [
442    ("PANEL_OUTLINE", PANEL_OUTLINE),
443    ("PANEL_DIGIT", PANEL_DIGIT),
444    ("PANEL_STACK_LIT", PANEL_STACK_LIT),
445    ("PANEL_HEAP_LIT", PANEL_HEAP_LIT),
446    ("PANEL_GAUGE_UNLIT", PANEL_GAUGE_UNLIT),
447];
448
449/// The integer parameters the RTL takes from here: `__N_<NAME>__` becomes the decimal number.
450pub fn verilog_numbers() -> Vec<(&'static str, u32)> {
451    vec![
452        ("COLS", COLS as u32),
453        ("ROWS", ROWS as u32),
454        ("SMALL_FIRST_ROW", SMALL_FIRST_ROW as u32),
455        ("SMALL_ROWS", SMALL_ROWS as u32),
456        ("SMALL_COLS", SMALL_COLS as u32),
457        ("PANEL_FIRST_ROW", (ROWS - PANEL_ROWS) as u32),
458        ("CLKS_PER_BIT", clocks_per_bit()),
459        ("ENGINE_CLKS_PER_BIT", ENGINE_CLKS_PER_BIT),
460        ("ENGINE_CLOCK_DIVIDER", ENGINE_CLOCK_DIVIDER),
461        ("ENGINE_LINK_CLKS_PER_BIT", ENGINE_CLKS_PER_BIT * ENGINE_CLOCK_DIVIDER),
462        ("ENGINE_FRAME_MAX", ENGINE_FRAME_MAX as u32),
463        ("CLOCKS_PER_PIXEL", CLOCKS_PER_PIXEL),
464        ("H_VISIBLE", H_TIMING.0 as u32),
465        ("H_FRONT", H_TIMING.1 as u32),
466        ("H_SYNC", H_TIMING.2 as u32),
467        ("H_BACK", H_TIMING.3 as u32),
468        ("V_VISIBLE", V_TIMING.0 as u32),
469        ("V_FRONT", V_TIMING.1 as u32),
470        ("V_SYNC", V_TIMING.2 as u32),
471        ("V_BACK", V_TIMING.3 as u32),
472        ("PANEL_TITLE_ROW", PANEL_TITLE_ROW as u32),
473        ("PANEL_LABEL_ROW", PANEL_LABEL_ROW as u32),
474        ("PANEL_BLOCK_ROW", PANEL_BLOCK_ROW as u32),
475        ("PANEL_CELL_ROW", PANEL_CELL_ROW as u32),
476        ("PANEL_CYCLES_COL", PANEL_CYCLES_COL as u32),
477        ("PANEL_BLOCK_COL", PANEL_BLOCK_COL as u32),
478        ("PANEL_BLOCK_TILE", PANEL_BLOCK_TILE as u32),
479        ("PANEL_CELL_COL", PANEL_CELL_COL as u32),
480        ("PANEL_STACK_COL", PANEL_STACK_COL as u32),
481        ("PANEL_HEAP_COL", PANEL_HEAP_COL as u32),
482        ("MAXBLK", super::wprog_engine_rtl::WPE_HW_MAXBLK as u32),
483        ("MAXCELL", super::wprog_engine_rtl::WPE_HW_MAXCELL as u32),
484        ("DEPTH", super::wprog_engine_rtl::WPE_HW_DEPTH as u32),
485        ("HEAP", super::wprog_engine_rtl::WPE_HW_HEAP as u32),
486    ]
487}
488
489/// Fill a terminal Verilog template from this module: `__V_<NAME>__` becomes a byte as a Verilog literal
490/// (`8'hC0`), `__X_<NAME>__` the hex a comment quotes (`0xC0`), `__N_<NAME>__` a decimal number, `__C_<NAME>__` a
491/// colour (`12'hFFF`), `__PALETTE_CASE__` the palette as `case` arms (`4'd1: palette = 12'h00A;`), `__HEAT_CASE__` the
492/// heat ramp the same way, and `__DIGIT_ROM_CASE__` the font's digit glyphs keyed `{digit, row}`. Panics on a
493/// placeholder this module does not define, so a template cannot name a value nobody owns.
494pub fn fill_verilog(template: &str) -> String {
495    let mut out = template.to_string();
496    for (name, byte) in VERILOG_BYTES {
497        out = out.replace(&format!("__V_{name}__"), &format!("8'h{byte:02X}"));
498        out = out.replace(&format!("__X_{name}__"), &format!("0x{byte:02X}"));
499    }
500    for (name, n) in verilog_numbers() {
501        out = out.replace(&format!("__N_{name}__"), &n.to_string());
502    }
503    for (name, rgb) in VERILOG_COLOURS {
504        out = out.replace(&format!("__C_{name}__"), &format!("12'h{rgb:03X}"));
505    }
506    let palette: Vec<String> =
507        PALETTE.iter().enumerate().map(|(i, rgb)| format!("      4'd{i}: palette = 12'h{rgb:03X};")).collect();
508    out = out.replace("__PALETTE_CASE__", &palette.join("\n"));
509    let heat: Vec<String> =
510        HEAT.iter().enumerate().map(|(i, rgb)| format!("      4'd{i}: heat_rgb = 12'h{rgb:03X};")).collect();
511    out = out.replace("__HEAT_CASE__", &heat.join("\n"));
512    if out.contains("__DIGIT_ROM_CASE__") {
513        let font = super::terminal_rtl::font();
514        let mut arms = Vec::new();
515        for d in 0..10u8 {
516            for row in 0..GLYPH_ROWS {
517                let bits = font[(b'0' + d) as usize * GLYPH_ROWS + row];
518                arms.push(format!("      8'h{d:X}{row:X}: digit_bits = 8'h{bits:02X};"));
519            }
520        }
521        out = out.replace("__DIGIT_ROM_CASE__", &arms.join("\n"));
522    }
523    for marker in ["__V_", "__X_", "__N_", "__C_", "__PALETTE", "__HEAT", "__DIGIT"] {
524        if let Some(at) = out.find(marker) {
525            let name: String = out[at..].chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_').collect();
526            panic!("a terminal template names a value terminal_protocol does not define: {name}");
527        }
528    }
529    out
530}
531
532/// The whole protocol as JSON, for a host that is not Rust.
533pub fn table_json() -> String {
534    let kinds = |all: &[(&str, u8)]| -> String {
535        all.iter().map(|(n, v)| format!("\"{n}\": {v}")).collect::<Vec<_>>().join(", ")
536    };
537    let palette: Vec<String> = PALETTE.iter().map(|c| c.to_string()).collect();
538    format!(
539        "{{\"version\": {VERSION}, \
540         \"slip\": {{\"end\": {SLIP_END}, \"esc\": {SLIP_ESC}, \"esc_end\": {SLIP_ESC_END}, \"esc_esc\": {SLIP_ESC_ESC}}}, \
541         \"host\": {{{}}}, \"board\": {{{}}}, \
542         \"ps2\": {{\"parity_error\": {PS2_PARITY_ERROR}, \"framing_error\": {PS2_FRAMING_ERROR}, \"no_ack\": {PS2_NO_ACK}}}, \
543         \"screen\": {{\"cols\": {COLS}, \"rows\": {ROWS}, \"panel_rows\": {PANEL_ROWS}, \"glyphs\": {GLYPHS}, \
544         \"small_first_row\": {SMALL_FIRST_ROW}, \"small_rows\": {SMALL_ROWS}, \"small_cols\": {SMALL_COLS}, \
545         \"glyph_rows\": {GLYPH_ROWS}, \"ui_glyphs\": \"{UI_GLYPHS}\", \"del_glyph\": \"{DEL_GLYPH}\", \
546         \"palette\": [{}]}}, \
547         \"panel\": {{\"title_row\": {PANEL_TITLE_ROW}, \"label_row\": {PANEL_LABEL_ROW}, \"block_row\": {PANEL_BLOCK_ROW}, \
548         \"cell_row\": {PANEL_CELL_ROW}, \"cycles_col\": {PANEL_CYCLES_COL}, \"block_col\": {PANEL_BLOCK_COL}, \
549         \"block_tile\": {PANEL_BLOCK_TILE}, \"cell_col\": {PANEL_CELL_COL}, \"stack_col\": {PANEL_STACK_COL}, \
550         \"heap_col\": {PANEL_HEAP_COL}, \"blocks\": {}, \"cells\": {}, \"depth\": {}, \"heap\": {}}}, \
551         \"features\": {{\"engine\": {FEATURE_ENGINE}, \"small\": {FEATURE_SMALL}, \"totals\": {FEATURE_TOTALS}}}, \"pong\": [{}], \"activity\": [{}], \"totals\": [{}], \"engine_frame_max\": {ENGINE_FRAME_MAX}, \"baud\": {BAUD}}}",
552        kinds(&host::ALL),
553        kinds(&board::ALL),
554        palette.join(", "),
555        super::wprog_engine_rtl::WPE_HW_MAXBLK,
556        super::wprog_engine_rtl::WPE_HW_MAXCELL,
557        super::wprog_engine_rtl::WPE_HW_DEPTH,
558        super::wprog_engine_rtl::WPE_HW_HEAP,
559        PONG_FIELDS.iter().map(|f| format!("\"{f}\"")).collect::<Vec<_>>().join(", "),
560        layout(&ACTIVITY_FIELDS),
561        layout(&TOTALS_FIELDS),
562    )
563}
564
565fn layout(fields: &[(&str, usize, usize)]) -> String {
566    fields
567        .iter()
568        .map(|(name, bytes, count)| format!("{{\"name\": \"{name}\", \"bytes\": {bytes}, \"count\": {count}}}"))
569        .collect::<Vec<_>>()
570        .join(", ")
571}