Skip to main content

logicaffeine_compile/codegen_sva/
terminal_rtl.rs

1//! The Fabric Terminal's RTL, emitted from its templates (`codegen_sva/terminal/`) with every protocol byte and
2//! dimension filled from [`super::terminal_protocol`]: the UART, SLIP framing, the PS/2 port, the VGA text plane,
3//! the link controller, the activity panel and the Nexys A7-100T top with its pins; and [`render`], the picture the
4//! screen's frames are held to. The board build reads the emitted files ([`board_files`]) from `benchmarks/silicon/`,
5//! where `fabric_terminal --emit` writes them.
6
7use super::terminal_protocol::fill_verilog;
8
9const UART: &str = include_str!("terminal/term_uart.v");
10const SLIP: &str = include_str!("terminal/term_slip.v");
11const PS2: &str = include_str!("terminal/term_ps2.v");
12const VGA: &str = include_str!("terminal/term_vga.v");
13const LINK: &str = include_str!("terminal/term_link.v");
14const ACTIVITY: &str = include_str!("terminal/term_activity.v");
15const TOP: &str = include_str!("terminal/term_top.v");
16const XDC: &str = include_str!("terminal/term_top.xdc");
17
18/// The font ROM the text plane reads with `$readmemh`, one glyph row per line (see `scripts/dev/gen-terminal-font.py`).
19pub const FONT_HEX: &str = include_str!("terminal/term_font.hex");
20/// The font's license, which travels with the ROM.
21pub const FONT_LICENSE: &str = include_str!("terminal/term_font.LICENSE");
22
23/// The module files a terminal top instantiates, in dependency order: (file name, filled Verilog). The activity
24/// panel belongs to a top with an engine.
25pub fn modules(engine: bool) -> Vec<(&'static str, String)> {
26    let mut files = vec![
27        ("term_uart.v", fill_verilog(UART)),
28        ("term_slip.v", fill_verilog(SLIP)),
29        ("term_ps2.v", fill_verilog(PS2)),
30        ("term_vga.v", fill_verilog(VGA)),
31        ("term_link.v", fill_verilog(LINK)),
32    ];
33    if engine {
34        files.push(("term_activity.v", fill_verilog(ACTIVITY)));
35    }
36    files
37}
38
39/// Keep the template text between `//__<keep>_BEGIN__` and `//__<keep>_END__`, and drop the `drop` section with
40/// its markers.
41fn select(text: &str, keep: &str, drop: &str) -> String {
42    let (kb, ke) = (format!("//__{keep}_BEGIN__\n"), format!("//__{keep}_END__\n"));
43    let (db, de) = (format!("//__{drop}_BEGIN__\n"), format!("//__{drop}_END__\n"));
44    let start = text.find(&db).unwrap_or_else(|| panic!("the terminal top has no {drop} section"));
45    let end = text[start..].find(&de).map(|e| start + e + de.len()).expect("an unterminated section");
46    let without = format!("{}{}", &text[..start], &text[end..]);
47    without.replace(&kb, "").replace(&ke, "")
48}
49
50/// The board top `top`: with `engine` naming the fabric engine module it instantiates — an engine emitted with its
51/// probes (`emit_wprog_engine_soc_verilog_hw_probed`), built beside it — or without one: the screen, keyboard and
52/// link alone, where ENGINE bytes are taken and dropped and the panel is not drawn.
53pub fn top(top: &str, engine: Option<&str>) -> String {
54    let text = match engine {
55        Some(module) => select(TOP, "ENGINE", "NO_ENGINE").replace("__ENGINE_MODULE__", module),
56        None => select(TOP, "NO_ENGINE", "ENGINE"),
57    };
58    fill_verilog(&text.replace("__TOP__", top))
59}
60
61/// The board's pin constraints for any terminal top.
62pub fn xdc() -> String {
63    XDC.to_string()
64}
65
66/// The fabric engine the Fabric Terminal instantiates: Board B's deployed engine, with its probes.
67pub const ENGINE_MODULE: &str = "wpe_soc_fpga_probed";
68/// The file [`board_files`] emits the engine into, beside the terminal's other files.
69pub const ENGINE_FILE: &str = "nexys_fabric_terminal_engine.v";
70
71/// Every file the board build reads from `benchmarks/silicon/`, as (file name, contents): the modules, the font
72/// ROM and its license, the pin constraints, the probed engine, and two tops — `nexys_fabric_terminal` with the
73/// engine and `nexys_terminal_shell`, the screen, keyboard and link alone.
74pub fn board_files() -> Vec<(String, String)> {
75    let mut out: Vec<(String, String)> = modules(true).into_iter().map(|(name, text)| (name.to_string(), text)).collect();
76    out.push(("term_font.hex".into(), FONT_HEX.into()));
77    out.push(("term_font8.hex".into(), font8().iter().map(|b| format!("{b:02x}\n")).collect()));
78    out.push(("term_font.LICENSE".into(), FONT_LICENSE.into()));
79    out.push(("nexys_fabric_terminal.xdc".into(), xdc()));
80    out.push((
81        ENGINE_FILE.to_string(),
82        super::wprog_engine_rtl::emit_wprog_engine_soc_verilog_hw_probed(
83            ENGINE_MODULE.strip_prefix("wpe_soc_").expect("an engine module is wpe_soc_<name>"),
84        ),
85    ));
86    out.push(("nexys_fabric_terminal.v".into(), top("nexys_fabric_terminal", Some(ENGINE_MODULE))));
87    out.push(("nexys_terminal_shell.v".into(), top("nexys_terminal_shell", None)));
88    out
89}
90
91/// The font ROM as bytes: glyph `g`, row `r` is `font()[g * 16 + r]`, the MSB its leftmost pixel.
92pub fn font() -> Vec<u8> {
93    FONT_HEX.lines().map(|l| u8::from_str_radix(l.trim(), 16).expect("the font ROM is hex bytes")).collect()
94}
95
96/// The small font ROM, made from [`font`]: glyph `g`, row `r` is `font8()[g * 8 + r]`, row
97/// `terminal_protocol::FONT8_ROWS[r]` of the 8×16 glyph.
98pub fn font8() -> Vec<u8> {
99    let font = font();
100    (0..font.len() / 16)
101        .flat_map(|g| super::terminal_protocol::FONT8_ROWS.iter().map(move |&r| (g, r)))
102        .map(|(g, r)| font[g * 16 + r])
103        .collect()
104}
105
106/// What the text plane is told to draw: every cell, the cursor and the pointer, the flash phase that blinking
107/// cells and the cursor follow, and the activity panel when the top has an engine.
108#[derive(Clone, Debug)]
109pub struct Screen {
110    /// `ROWS × COLS` cells, row-major.
111    pub cells: Vec<super::terminal_protocol::Cell>,
112    /// The small plane: `SMALL_ROWS × SMALL_COLS` cells, row-major.
113    pub small: Vec<super::terminal_protocol::Cell>,
114    /// The small-text window as `(row, col, rows, cols)` in text cells, when open.
115    pub window: Option<(u8, u8, u8, u8)>,
116    pub cursor: Option<(u8, u8)>,
117    pub pointer: Option<(u8, u8)>,
118    pub flash: bool,
119    pub panel: Option<Panel>,
120}
121
122impl Screen {
123    /// The screen at power-on: every cell a light-grey space on black.
124    pub fn blank() -> Self {
125        use super::terminal_protocol::{Cell, COLS, ROWS, SMALL_COLS, SMALL_ROWS};
126        Screen {
127            cells: vec![Cell::new(b' ', 7, 0); ROWS as usize * COLS as usize],
128            small: vec![Cell::new(b' ', 7, 0); SMALL_ROWS as usize * SMALL_COLS as usize],
129            window: None,
130            cursor: None,
131            pointer: None,
132            flash: false,
133            panel: None,
134        }
135    }
136}
137
138/// What one frame of the activity panel shows: the engine's state at the frame's snapshot, and the engine clocks
139/// each block and each cell ran in the frame before it.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct Panel {
142    /// One count per engine block (`WPE_HW_MAXBLK`).
143    pub block_hits: Vec<u32>,
144    /// One count per program cell (`WPE_HW_MAXCELL`).
145    pub cell_hits: Vec<u32>,
146    pub running: bool,
147    pub pc: u16,
148    pub cell: u16,
149    pub sp: u16,
150    pub hp: u16,
151    pub cycles: u32,
152}
153
154impl Panel {
155    /// The panel before any program: nothing counted, the engine idle.
156    pub fn idle() -> Self {
157        use super::wprog_engine_rtl::{WPE_HW_MAXBLK, WPE_HW_MAXCELL};
158        Panel {
159            block_hits: vec![0; WPE_HW_MAXBLK],
160            cell_hits: vec![0; WPE_HW_MAXCELL],
161            running: false,
162            pc: 0,
163            cell: 0,
164            sp: 0,
165            hp: 0,
166            cycles: 0,
167        }
168    }
169
170    /// The panel's colour at screen pixel (`x`, `y`), or `None` where the text shows through. Inside its regions:
171    /// - the cycle count: ten digit glyphs from the font, [`PANEL_DIGIT`] on black, leading zeros black, the units
172    ///   digit always drawn;
173    /// - a block tile or cell square: black but for an inner box two pixels in, painted with the [`HEAT`] colour of
174    ///   its count, and the box's one-pixel border [`PANEL_OUTLINE`] when the engine is running in it;
175    /// - a stack segment: eight pixels wide, a 6 × 10 box one pixel in from the left and three down, lit
176    ///   ([`PANEL_STACK_LIT`]) for each frame below `sp`, else [`PANEL_GAUGE_UNLIT`];
177    /// - a heap column: one pixel wide, eight tall from four down, lit ([`PANEL_HEAP_LIT`]) below `hp`.
178    ///
179    /// [`PANEL_DIGIT`]: super::terminal_protocol::PANEL_DIGIT
180    /// [`HEAT`]: super::terminal_protocol::HEAT
181    /// [`PANEL_OUTLINE`]: super::terminal_protocol::PANEL_OUTLINE
182    /// [`PANEL_STACK_LIT`]: super::terminal_protocol::PANEL_STACK_LIT
183    /// [`PANEL_GAUGE_UNLIT`]: super::terminal_protocol::PANEL_GAUGE_UNLIT
184    /// [`PANEL_HEAP_LIT`]: super::terminal_protocol::PANEL_HEAP_LIT
185    pub fn pixel(&self, font: &[u8], x: usize, y: usize) -> Option<u16> {
186        use super::terminal_protocol::{
187            heat_level, GLYPH_ROWS, HEAT, PANEL_BLOCK_COL, PANEL_BLOCK_ROW, PANEL_BLOCK_TILE, PANEL_CELL_COL,
188            PANEL_CELL_ROW, PANEL_CYCLES_COL, PANEL_DIGIT, PANEL_GAUGE_UNLIT, PANEL_HEAP_COL, PANEL_HEAP_LIT,
189            PANEL_OUTLINE, PANEL_STACK_COL, PANEL_STACK_LIT, PANEL_TITLE_ROW,
190        };
191        use super::wprog_engine_rtl::{WPE_HW_DEPTH, WPE_HW_HEAP};
192        let row = y / GLYPH_ROWS;
193        let gy = y % GLYPH_ROWS;
194        let span = |col: u8, width: usize| (x >= col as usize * 8 && x < col as usize * 8 + width).then(|| x - col as usize * 8);
195        let heat_box = |size: usize, hits: u32, current: bool, sub: usize| {
196            let inner = sub >= 2 && sub < size - 2 && (2..14).contains(&gy);
197            let rim = sub == 2 || sub == size - 3 || gy == 2 || gy == 13;
198            if !inner {
199                0
200            } else if current && rim {
201                PANEL_OUTLINE
202            } else {
203                HEAT[heat_level(hits)]
204            }
205        };
206        if row == PANEL_TITLE_ROW as usize {
207            let dx = span(PANEL_CYCLES_COL, 80)?;
208            let place = 9 - dx / 8;
209            let above = self.cycles as u64 / 10u64.pow(place as u32);
210            let digit = (above % 10) as u8;
211            let bits = font[(b'0' + digit) as usize * GLYPH_ROWS + gy];
212            let on = (above != 0 || place == 0) && (bits >> (7 - dx % 8)) & 1 == 1;
213            return Some(if on { PANEL_DIGIT } else { 0 });
214        }
215        if row == PANEL_BLOCK_ROW as usize {
216            let tile = PANEL_BLOCK_TILE as usize * 8;
217            if let Some(dx) = span(PANEL_BLOCK_COL, self.block_hits.len() * tile) {
218                let b = dx / tile;
219                return Some(heat_box(tile, self.block_hits[b], self.running && self.pc as usize == b, dx % tile));
220            }
221            if let Some(dx) = span(PANEL_STACK_COL, WPE_HW_DEPTH * 8) {
222                let inner = (1..7).contains(&(dx % 8)) && (3..13).contains(&gy);
223                let lit = dx / 8 < self.sp as usize;
224                return Some(if !inner { 0 } else if lit { PANEL_STACK_LIT } else { PANEL_GAUGE_UNLIT });
225            }
226            if let Some(dx) = span(PANEL_HEAP_COL, WPE_HW_HEAP) {
227                let inner = (4..12).contains(&gy);
228                let lit = dx < self.hp as usize;
229                return Some(if !inner { 0 } else if lit { PANEL_HEAP_LIT } else { PANEL_GAUGE_UNLIT });
230            }
231            return None;
232        }
233        if row == PANEL_CELL_ROW as usize {
234            let dx = span(PANEL_CELL_COL, self.cell_hits.len() * 16)?;
235            let c = dx / 16;
236            return Some(heat_box(16, self.cell_hits[c], self.running && self.cell as usize == c, dx % 16));
237        }
238        None
239    }
240}
241
242/// The picture the screen draws for `screen`: 640 × 480 pixels, row-major, each the 12-bit 0xRGB the board's DAC is
243/// driven with — the text plane (the small-text window in the small font), and the activity panel over it. This is
244/// the model the RTL's rendered frames are held to.
245pub fn render(screen: &Screen) -> Vec<u16> {
246    use super::terminal_protocol::{COLS, GLYPH_ROWS, H_TIMING, PALETTE, SMALL_COLS, V_TIMING};
247    let (font, font8) = (font(), font8());
248    let (w, h) = (H_TIMING.0 as usize, V_TIMING.0 as usize);
249    let mut out = Vec::with_capacity(w * h);
250    for y in 0..h {
251        for x in 0..w {
252            let (row, col) = ((y / GLYPH_ROWS) as u8, (x / 8) as u8);
253            let in_window = screen
254                .window
255                .filter(|&(r, c, rs, cs)| rs > 0 && (r..r + rs).contains(&row) && (c..c + cs).contains(&col));
256            let (cell, bits) = match in_window {
257                Some((r, c, _, _)) => {
258                    let srow = (row - r) as usize * 2 + (y % GLYPH_ROWS) / 8;
259                    let cell = screen.small[srow * SMALL_COLS as usize + (col - c) as usize];
260                    (cell, font8[cell.glyph as usize * 8 + y % 8])
261                }
262                None => {
263                    let cell = screen.cells[row as usize * COLS as usize + col as usize];
264                    (cell, font[cell.glyph as usize * GLYPH_ROWS + y % GLYPH_ROWS])
265                }
266            };
267            let on = (bits >> (7 - x % 8)) & 1 == 1 && !(cell.blink && !screen.flash);
268            let underline = screen.cursor == Some((row, col)) && screen.flash && y % GLYPH_ROWS >= 14;
269            let lit = (on || underline) ^ (screen.pointer == Some((row, col)));
270            let text = PALETTE[if lit { cell.fg } else { cell.bg } as usize];
271            out.push(screen.panel.as_ref().and_then(|p| p.pixel(&font, x, y)).unwrap_or(text));
272        }
273    }
274    out
275}