Skip to content

terminal_top

Source: Verilog/Terminals/rtl/terminal_top.v

Where it sits (Nexys): nd120_nexys4ddr_top > terminal_top - instance path: TERMINAL

Used in: nd120_console_mega65 (MEGA65 R6, MEGA65 R3), nd120_console_mister (MiSTer), nd120_nexys4ddr_top (Nexys)

Contains: byte_fifo, cdc_byte, char_ram, rate_meter x2, term_panel, terminal_ctrl_tdv, text_screen

Module hierarchy - All modules

terminal_top symbol

Schematic

Drawn from the Verilog: the yosys netlist of the Nexys 4 DDR build, instance TERMINAL. Sub-modules are boxes (click the picture to open it full size; there every sub-module box links to its page, and every wire shows its Verilog name).

terminal_top schematic

Parameters

Parameter Default
COLS 80
ROWS 24
AWIDTH 11
FONT_FILE "../font/font8x16.hex"
CELL_W 8
CELL_H 16
WITH_PANEL 1
H_VISIBLE 800
H_FRONT_PORCH 40
H_SYNC 128
H_BACK_PORCH 88
V_VISIBLE 600
V_FRONT_PORCH 1
V_SYNC 4
V_BACK_PORCH 23
ORIGIN_X (H_VISIBLE - COLS * CELL_W
ORIGIN_Y (V_VISIBLE - ROWS * CELL_H

Ports

Direction Width Name Description
input 1 byte_clk
input 1 byte_rst_n (active low)
input 1 byte_valid
input [7:0] byte_data
output 1 byte_ready low while the previous byte is in flight
input 1 national
input 1 mode
input 1 pix_clk
input 1 pix_rst_n (active low)
output 1 pixel 1 = ink, 0 = paper (the console text alone)
output [2:0] colour
output 1 panel_active
output 1 hsync
output 1 vsync
output 1 de
output 1 hblank
output 1 vblank
input 1 panel_enable
input [3:0] panel_pil current program level
input [15:0] panel_actlv ACTIVE LEVEL word from the panel processor (0 = none yet)
input 1 panel_lev0 LEV0 - running at level 0, i.e. idle
input 1 panel_hit HIT - the ND-120's own cache
input [1:0] panel_ring PCR protect ring
input 1 panel_paging_on PONI
input 1 panel_interrupt_on IONI
input 1 panel_running CPU running (already de-inverted)
input 1 panel_cpu_red
input 1 panel_cpu_green
input 1 panel_hdd_rd
input 1 panel_hdd_wr
input 1 panel_flp_rd
input 1 panel_flp_wr
input [15:0] panel_mips
output 1 bell one pix_clk per BEL received
output [3:0] leds DECLL (CSI Ps q) - the VT100 keyboard lamps L1-L4
output 1 dbg_box_mode
output 1 dbg_saw_esc6

Verilog source

Verilog/Terminals/rtl/terminal_top.v on GitHub.

Show the Verilog of terminal_top (643 lines)
//============================================================================
//! Terminal core - top level. Bytes in, video out.
//!
//! Part of the board-independent terminal core (Verilog/Terminals/).
//! Plan: Verilog/Terminals/docs/PLAN-vt100-terminal-core.md
//!
//! This is the whole Stage A terminal: give it console bytes on any clock and
//! it gives you a monochrome pixel stream with sync. It knows nothing about
//! any board - no VGA connector, no HDMI scaler, no keyboard. Each board wires
//! the two ends:
//!
//!   Nexys 4 DDR : deserialize cpu_txd into byte_in; pixel -> the 12-bit VGA
//!                 pins; keyboard from the onboard USB host as plain PS/2
//!   MiSTer      : ps2_key from hps_io; pixel -> CLK_VIDEO/CE_PIXEL/VGA_*
//!   MEGA65      : mega65 keyboard matrix; pixel -> the VDAC
//!
//! Keyboard input is NOT handled here - a keyboard produces bytes that go back
//! to the machine, which is the board's business, not the screen's.
//!
//! Written 27-AUG-2026.
//============================================================================

`default_nettype none

module terminal_top #(
    parameter integer COLS      = 80,
    parameter integer ROWS      = 24,
    parameter integer AWIDTH    = 11,
    parameter         FONT_FILE = "../font/font8x16.hex",

    //! Glyph box. Set by the font, not a free choice - font8x16.hex is 8 wide
    //! and 16 tall. Parameters here only so the origins below can be derived
    //! from them; change the font before changing these.
    parameter integer CELL_W = 8,
    parameter integer CELL_H = 16,

    //! 0 removes the operator panel from the design ENTIRELY - the renderer,
    //! its font ROM, the rate meters and the uptime counter all disappear.
    //!
    //! This is NOT the same as the board's panel switch. That switch blanks the
    //! panel at run time and the logic stays in the bitstream; this parameter is
    //! what you reach for when fabric or timing is actually tight. Both exist
    //! because they answer different questions.
    parameter integer WITH_PANEL = 1,

    // VGA mode - defaults are 800x600@60 from a 40.000 MHz pixel clock
    parameter integer H_VISIBLE     = 800,
    parameter integer H_FRONT_PORCH = 40,
    parameter integer H_SYNC        = 128,
    parameter integer H_BACK_PORCH  = 88,
    parameter integer V_VISIBLE     = 600,
    parameter integer V_FRONT_PORCH = 1,
    parameter integer V_SYNC        = 4,
    parameter integer V_BACK_PORCH  = 23,
    //! Top-left corner of the character grid inside the visible area.
    //!
    //! COMPUTED, not written down, and that is the point. These were constants
    //! until 28-AUG-2026, and ORIGIN_Y still said 108 - the value that centres
    //! a 24-ROW grid - long after the terminal became the TDV2200's real
    //! 80x25. It was not harmless: terminal_top passes its OWN defaults down,
    //! so 108 OVERRODE the corrected 100 in text_screen.v, and no board
    //! overrides it, so every build would have drawn the grid 8 pixels low.
    //!
    //! text_screen_tb did not catch it because it passes 100 explicitly - a
    //! test that supplies its own value cannot check a default. Deriving the
    //! number from the ones it depends on removes the whole failure mode:
    //! change ROWS, COLS or the video mode and the grid re-centres itself.
    parameter integer ORIGIN_X      = (H_VISIBLE - COLS * CELL_W) / 2,
    parameter integer ORIGIN_Y      = (V_VISIBLE - ROWS * CELL_H) / 2
) (
    // Byte in - the console stream, on ITS OWN clock
    input  wire       byte_clk,
    input  wire       byte_rst_n,
    input  wire       byte_valid,
    input  wire [7:0] byte_data,
    output wire       byte_ready,  //! low while the previous byte is in flight

    //! Font page: 0 = US / ISO 646 IRV, 1 = Norwegian (NS 4551-1). Must be
    //! driven from the SAME source as the keyboard's layout_no - the two
    //! disagreeing is worse than either being wrong, because what you type and
    //! what you see stop matching.
    input  wire national,

    //! Video mode: 0 = the H_*/V_* parameters at 1x glyphs, 1 = H2_*/V2_* at
    //! 2x. The board MUST switch the pixel clock with it - this core only
    //! counts, it cannot know what the clock really is.
    input  wire mode,

    // Video out - pixel clock domain
    input  wire pix_clk,
    input  wire pix_rst_n,
    output wire pixel,   //! 1 = ink, 0 = paper (the console text alone)

    //! Palette index for this pixel. The BOARD maps it to its own colour
    //! depth, so nothing board-specific lives in this core:
    //!   0 black  1 text ink  2 fascia  3 silkscreen
    //!   4 LCD ground  5 LCD segment  6 lit legend  7 unlit legend
    output wire [2:0] colour,
    //! 1 while this pixel belongs to the operator panel (not the console text
    //! or banner). A board that lets the user recolour the console-text
    //! palette index can gate on this so the panel's own green CPU lamp,
    //! which uses the same index, stays fixed. Safe to leave unconnected.
    output wire       panel_active,
    output wire hsync,
    output wire vsync,
    output wire de,
    //! `de` split in two, for a board whose framework wants them separately
    //! (MiSTer2MEGA65). Same two-clock alignment as `de`. Added 02-SEP-2026;
    //! leave unconnected where only `de` is needed.
    output wire hblank,
    output wire vblank,

    //! ---- operator panel -------------------------------------------------
    //! Drawn in the empty area below the text grid. `panel_enable` low draws
    //! nothing; it does NOT remove the logic, which is a build-time choice.
    //! RAW machine signals, exactly as they leave ND3202D's DBG_PANEL port in
    //! MC68705 Port-D order. The rate meters and the uptime counter live in
    //! here rather than in each board's top level, so three boards cannot end
    //! up with three slightly different definitions of "utilization".
    input  wire       panel_enable,
    input  wire [3:0] panel_pil,        //! current program level
    input  wire [15:0] panel_actlv,     //! ACTIVE LEVEL word from the panel processor (0 = none yet)
    input  wire       panel_lev0,       //! LEV0 - running at level 0, i.e. idle
    input  wire       panel_hit,        //! HIT  - the ND-120's own cache
    //! High while the lookup HIT belongs to is happening. Without it there is
    //! no denominator and the CACHE HIT RATE bar cannot mean anything.

    input  wire [1:0] panel_ring,       //! PCR protect ring
    input  wire       panel_paging_on,  //! PONI
    input  wire       panel_interrupt_on, //! IONI
    input  wire       panel_running,    //! CPU running (already de-inverted)

    //! Disc access strobes - Winchester and floppy, read and write. Held and
    //! displayed by the panel; see term_panel.v.
    //! CPU board lamps - LED[0] RED (MACL in progress) and LED[1] GREEN
    //! (initialisation complete, written only at MACL2 = self-test passed).
    input  wire       panel_cpu_red,
    input  wire       panel_cpu_green,
    input  wire       panel_hdd_rd,
    input  wire       panel_hdd_wr,
    input  wire       panel_flp_rd,
    input  wire       panel_flp_wr,

    //! MIPS word from the board's mips_counter (CPU clock domain), {d3,d2,
    //! d1,d0} BCD. Boards without a counter tie it to 0 and the panel shows
    //! 00.00. Synced below exactly as ACTLV is.
    input  wire [15:0] panel_mips,

    output wire bell,       //! one pix_clk per BEL received
    output wire [3:0] leds, //! DECLL (CSI Ps q) - the VT100 keyboard lamps L1-L4

    //! Box-charset debug taps (01-SEP-2026, TDV only - tied 0 on VT100,
    //! which has no NDSS6 concept). See terminal_ctrl_tdv.v's port comment.
    output wire dbg_box_mode,
    output wire dbg_saw_esc6
);

  //--------------------------------------------------------------------------
  // Byte into the pixel domain
  //--------------------------------------------------------------------------

  wire       s_pix_byte_valid;
  wire [7:0] s_pix_byte_data;
  wire       s_fifo_in_ready;
  wire       s_ctrl_byte_valid;
  wire [7:0] s_ctrl_byte_data;
  //! terminal_ctrl is not always able to take a byte - a clear-screen sweep
  //! walks 1920 cells and a DECSTBM region scroll ~3.8k clocks, holding this
  //! low throughout. See the CDC note and byte_fifo.v.
  wire       s_ctrl_ready;

  cdc_byte CDC (
      .src_clk  (byte_clk),
      .src_rst_n(byte_rst_n),
      .src_valid(byte_valid),
      .src_data (byte_data),
      .src_ready(byte_ready),

      .dst_clk  (pix_clk),
      .dst_rst_n(pix_rst_n),
      .dst_valid(s_pix_byte_valid),
      .dst_data (s_pix_byte_data),
      .dst_ready(s_fifo_in_ready)
  );

  //! Elastic buffer between the crossing and the controller. A VT100 region
  //! scroll runs ~96 us at 40 MHz - longer than one 115200-baud byte time -
  //! and the console UART receiver does not respect ready, so without slack
  //! here a byte arriving mid-scroll would be lost. See byte_fifo.v.
  byte_fifo FIFO (
      .clk      (pix_clk),
      .rst_n    (pix_rst_n),
      .in_valid (s_pix_byte_valid),
      .in_data  (s_pix_byte_data),
      .in_ready (s_fifo_in_ready),
      .out_valid(s_ctrl_byte_valid),
      .out_data (s_ctrl_byte_data),
      .out_ready(s_ctrl_ready)
  );

  //--------------------------------------------------------------------------
  // Character RAM - written by the control logic, read by the screen
  //--------------------------------------------------------------------------

  wire              s_we;
  wire [AWIDTH-1:0] s_waddr;
  wire [      15:0] s_wdata;
  wire [AWIDTH-1:0] s_raddr;
  wire [      15:0] s_rdata;
  wire [AWIDTH-1:0] s_raddr2;
  wire [      15:0] s_rdata2;

  char_ram #(
      .COLS  (COLS),
      .ROWS  (ROWS),
      .AWIDTH(AWIDTH)
  ) CHARRAM (
      .clk   (pix_clk),
      .we    (s_we),
      .waddr (s_waddr),
      .wdata (s_wdata),
      .raddr2(s_raddr2),
      .rdata2(s_rdata2),
      .raddr (s_raddr),
      .rdata (s_rdata)
  );

  //--------------------------------------------------------------------------
  // Screen state
  //--------------------------------------------------------------------------

  wire [7:0] s_top_row;
  wire [7:0] s_cursor_col;
  wire [7:0] s_cursor_row;
  wire       s_cursor_enable;
  wire       s_frame_end;
  wire       s_rev_screen;
  wire       s_blink_on;

  // Two separate, compile-time-selected controllers - VT100 (type 6) and
  // TDV2200 (type 93), never both in the same build. Default TDV2200: PED
  // and LED are built for the Tandberg keyboard's own key set, not VT100
  // CSI sequences (Verilog/Terminals/docs/SPEC-tdv2200.md has the full
  // account of why). Build with -DND120_TERMINAL_VT100 for the VT100
  // variant instead. Same port list either way - only the module name (and
  // the caller's ROWS, 24 vs 25) differs, so this is the one place that
  // needs to know which one exists.
`ifdef ND120_TERMINAL_VT100
  assign dbg_box_mode  = 1'b0;
  assign dbg_saw_esc6  = 1'b0;
  terminal_ctrl #(
      .COLS  (COLS),
      .ROWS  (ROWS),
      .AWIDTH(AWIDTH)
  ) CTRL (
`else
  terminal_ctrl_tdv #(
      .COLS  (COLS),
      .ROWS  (ROWS),
      .AWIDTH(AWIDTH)
  ) CTRL (
      .dbg_box_mode(dbg_box_mode),
      .dbg_saw_esc6(dbg_saw_esc6),
`endif
      .clk  (pix_clk),
      .rst_n(pix_rst_n),

      .byte_valid(s_ctrl_byte_valid),
      .byte_data (s_ctrl_byte_data),
      // FED BACK, since 28-AUG-2026 (and since 30-AUG through the FIFO). This
      // used to be left unconnected on the argument that a 115200 console
      // byte (every ~87 us) could never catch the ~48 us clear-screen window.
      // True of a UART, and false the moment term_banner.v became a source:
      // it hands over a byte every ~150 ns and the power-on clear ate the
      // entire startup message.
      .ready     (s_ctrl_ready),

      .ram_we    (s_we),
      .ram_waddr (s_waddr),
      .ram_wdata (s_wdata),
      .ram_raddr2(s_raddr2),
      .ram_rdata2(s_rdata2),

      .top_row      (s_top_row),
      .cursor_col   (s_cursor_col),
      .cursor_row   (s_cursor_row),
      .cursor_enable(s_cursor_enable),
      .rev_screen   (s_rev_screen),
      .blink_on     (s_blink_on),

      .frame_end(s_frame_end),
      .bell     (bell),
      .leds     (leds)
  );

  wire s_screen_pixel;
  assign pixel = s_screen_pixel;

  //! The panel works from the raw counters and applies its own two clocks of
  //! delay, matching the text pipeline. It never overlaps the text grid, so
  //! there is no arbitration - just a priority in the final mux.
  wire [11:0] s_x_raw, s_y_raw;
  wire        s_panel_active;
  wire [2:0]  s_panel_colour;

  generate
    if (WITH_PANEL != 0) begin : g_panel
    //------------------------------------------------------------------
    // CLOCK-DOMAIN CROSSING for every panel signal
    //
    // These all come from the CPU clock domain and are sampled here in the
    // PIXEL domain. Reading them directly - which is what this did until
    // 28-AUG-2026 - is a genuine CDC violation, and PIL showed exactly why.
    //
    // PIL is four bits that change together. Sampled asynchronously they
    // arrive SKEWED, so a transition like 7 -> 8 (0111 -> 1000) is seen as
    // whatever intermediate codes the individual bits happen to produce -
    // including 1111. That transient got latched into the afterglow and lit
    // level 15, on a machine that never uses level 15. Reported from hardware
    // as "level 15 lights a lot and level 15 is NEVER used", which is precisely
    // what a skewed multi-bit crossing looks like once something downstream
    // remembers what it saw.
    //
    // Two flops for metastability, then - for the multi-bit bus - a stability
    // gate: a value is only accepted once it has held still for a while.
    //
    // TWO SAMPLES WAS NOT ENOUGH - 28-AUG-2026. The first version accepted a
    // value as soon as two consecutive samples agreed, and compared one of
    // them against s_pil_m, the FIRST flop of the synchroniser, which is the
    // one allowed to be metastable. Comparing against a metastable flop is not
    // a stability test at all.
    //
    // It also could not reject what PIL actually does. PIL is STS[11:8]
    // (CGA_ALU.v:134) - the level field of a status register that is selected
    // PER LEVEL, so on a level change the bus settles through intermediate
    // codes before it lands. At 40 MHz pixel clock against a 16.67 MHz CPU
    // clock, one CPU cycle of transient spans about 2.4 pixel samples, which
    // walks straight through a two-sample gate. Downstream, term_panel USED TO
    // reload an afterglow counter for pil on EVERY clock, so a single transient
    // code lit that lamp for the whole ~1 s afterglow. Ronny's report -
    // levels walking 2,3,4,5,6 and "every time the level changes all the
    // levels light up" - looked like exactly that shape.
    //
    // So: three flops, the comparison taken between the SECOND and THIRD (both
    // past the metastable one), and the value accepted only after it has been
    // identical for 8 consecutive samples - 200 ns at 40 MHz, comfortably
    // longer than a CPU cycle. Any code the bus merely passes through on its
    // way somewhere else is discarded.
    //
    // MEASURED ON HARDWARE 29-AUG-2026, AND IT WAS NOT THE CAUSE. Two ILA
    // captures of s_ila_pil on the Nexys (TPE INSTRUCTION test) show a level
    // change as ONE clean transition with no intermediate codes - the bus is
    // fine. What the captures did show is PIL genuinely pulsing to 12, 13,
    // 14, 15 for 15 CPU clocks each (~1 us), and the 63-frame afterglow in
    // term_panel stretched each 1 us blip into a lamp lit for a second. That
    // afterglow is gone - term_panel now shows per-frame occupancy. This gate
    // stays because it is harmless and the reasoning above about a transient
    // code is still sound for a bus that does glitch; it just fixes nothing
    // here.
    //------------------------------------------------------------------
    localparam [2:0] PIL_STABLE_SAMPLES = 3'd7;   //! 8 samples: 0..7

    reg [3:0] s_pil_m, s_pil_s, s_pil_t, s_pil_q;
    reg [2:0] s_pil_cnt;
    reg [1:0] s_lev0_sync, s_hit_sync;
    reg [1:0] s_pag_sync, s_int_sync, s_run_sync;
    reg [1:0] s_hrd_sync, s_hwr_sync, s_frd_sync, s_fwr_sync;
    reg [1:0] s_cred_sync, s_cgrn_sync;
    reg [1:0] s_ring_m, s_ring_s, s_ring_q;

    always @(posedge pix_clk or negedge pix_rst_n) begin
      if (!pix_rst_n) begin
        s_pil_m <= 4'd0; s_pil_s <= 4'd0; s_pil_t <= 4'd0; s_pil_q <= 4'd0;
        s_pil_cnt <= 3'd0;
        s_ring_m <= 2'd0; s_ring_s <= 2'd0; s_ring_q <= 2'd0;
        s_lev0_sync <= 2'd0; s_hit_sync <= 2'd0;
        s_pag_sync  <= 2'd0; s_int_sync <= 2'd0; s_run_sync  <= 2'd0;
        s_cred_sync <= 2'd0; s_cgrn_sync <= 2'd0;
        s_hrd_sync  <= 2'd0; s_hwr_sync <= 2'd0;
        s_frd_sync  <= 2'd0; s_fwr_sync <= 2'd0;
      end else begin
        s_pil_m <= panel_pil;
        s_pil_s <= s_pil_m;
        s_pil_t <= s_pil_s;
        //! Compare the two flops PAST the metastable one, and only publish a
        //! value that has survived 8 consecutive identical samples.
        if (s_pil_t == s_pil_s) begin
          if (s_pil_cnt != PIL_STABLE_SAMPLES) s_pil_cnt <= s_pil_cnt + 3'd1;
          else                                 s_pil_q   <= s_pil_t;
        end else begin
          s_pil_cnt <= 3'd0;
        end

        s_ring_m <= panel_ring;
        s_ring_s <= s_ring_m;
        if (s_ring_s == s_ring_m) s_ring_q <= s_ring_s;

        s_lev0_sync <= {s_lev0_sync[0], panel_lev0};
        s_hit_sync  <= {s_hit_sync[0],  panel_hit};
        s_pag_sync  <= {s_pag_sync[0],  panel_paging_on};
        s_int_sync  <= {s_int_sync[0],  panel_interrupt_on};
        s_run_sync  <= {s_run_sync[0],  panel_running};
        s_cred_sync <= {s_cred_sync[0], panel_cpu_red};
        s_cgrn_sync <= {s_cgrn_sync[0], panel_cpu_green};
        s_hrd_sync  <= {s_hrd_sync[0],  panel_hdd_rd};
        s_hwr_sync  <= {s_hwr_sync[0],  panel_hdd_wr};
        s_frd_sync  <= {s_frd_sync[0],  panel_flp_rd};
        s_fwr_sync  <= {s_fwr_sync[0],  panel_flp_wr};
      end
    end

    wire [3:0] w_pil  = s_pil_q;

    // ACTLV crosses from the CPU clock too. It is a 16-bit word that changes
    // every 20 ms, so plain two-flop synchronisers per bit are enough: a bit
    // caught mid-change is right on the next frame anyway.
    reg [15:0] s_actlv_m, s_actlv_s;
    always @(posedge pix_clk or negedge pix_rst_n) begin
      if (!pix_rst_n) begin
        s_actlv_m <= 16'd0; s_actlv_s <= 16'd0;
      end else begin
        s_actlv_m <= panel_actlv;
        s_actlv_s <= s_actlv_m;
      end
    end
    wire [15:0] w_actlv = s_actlv_s;

    // The MIPS word changes once a second - the same treatment as ACTLV.
    reg [15:0] s_mips_m, s_mips_s;
    always @(posedge pix_clk or negedge pix_rst_n) begin
      if (!pix_rst_n) begin
        s_mips_m <= 16'd0; s_mips_s <= 16'd0;
      end else begin
        s_mips_m <= panel_mips;
        s_mips_s <= s_mips_m;
      end
    end
    wire [15:0] w_mips = s_mips_s;
    wire [1:0] w_ring = s_ring_q;
    wire w_lev0 = s_lev0_sync[1];
    wire w_hit  = s_hit_sync[1];
    wire w_pag  = s_pag_sync[1];
    wire w_int  = s_int_sync[1];
    wire w_run  = s_run_sync[1];
    wire w_cred = s_cred_sync[1];
    wire w_cgrn = s_cgrn_sync[1];
    wire w_hrd  = s_hrd_sync[1];
    wire w_hwr  = s_hwr_sync[1];
    wire w_frd  = s_frd_sync[1];
    wire w_fwr  = s_fwr_sync[1];

    //! UTILIZATION is the inverse of LEV0: the real panel's caption is how much
    //! time the machine was NOT idle, and idle on an ND is "running at level 0".
    wire [3:0] s_utilization;
    //! WINDOW_BITS 24 is ~420 ms per step at 40 MHz rather than the default
    //! 22's ~105 ms. Ronny's recollection of the real machine is that
    //! UTILIZATION moved about twice a second at most; ten times a second
    //! reads as noise rather than as a measurement. PEAK_HOLD lets it rise at
    //! once and fall one eighth per window, so bursts of work stay visible.
    rate_meter #(.PEAK_HOLD(1), .WINDOW_BITS(24)) UTIL_METER (
        .clk(pix_clk), .rst_n(pix_rst_n), .sample(!w_lev0), .eighths(s_utilization)
    );

    wire [3:0] s_cache_hit;
    //! MEASURED THE WAY THE MACHINE MEASURED IT. The input is LHIT, the
    //! latched Load Hit that the real panel's MC68705 reads on Port D bit 4 -
    //! the same treatment UTILIZATION gets from LEV0 on bit 5.
    //!
    //! A duty cycle is the honest model here, not a hits-per-lookup ratio.
    //! The 68705 SAMPLES Port D periodically and works the rate out from LHIT
    //! alone over time; it never had a lookup count to divide by. An earlier
    //! version here invented one (LAPA_n as a denominator, through
    //! ratio_meter) because a duty cycle taken on the cache's raw comparator
    //! output sat near empty. That was solving the wrong problem: the raw
    //! output was the wrong signal, and LHIT is asserted for a whole load
    //! cycle rather than one clock.
    //!
    //! Same window and peak hold as UTILIZATION so the two bars move at the
    //! same speed and can be read together. If it reads persistently low once
    //! the cache actually works, WINDOW_BITS is the knob - but check the cache
    //! first, because with CUP dead this reads a true zero.
    rate_meter #(.PEAK_HOLD(1), .WINDOW_BITS(24)) HIT_METER (
        .clk(pix_clk), .rst_n(pix_rst_n), .sample(w_hit), .eighths(s_cache_hit)
    );

    //! Uptime, counted in FRAMES rather than clocks. The frame rate is 60 Hz in
    //! both video modes while the pixel clock is not - 40 MHz at 800x600 and
    //! 148.5 MHz at 1080p - so counting frames keeps the clock correct when the
    //! resolution switch is thrown. Counting clocks would have made the panel run
    //! 3.7x fast in one of the two modes, which is the kind of bug that gets
    //! blamed on the machine.
    reg [5:0]  s_up_frames;
    reg [5:0]  s_up_sec;
    reg [5:0]  s_up_min;
    reg [4:0]  s_up_hr;

    always @(posedge pix_clk or negedge pix_rst_n) begin
      if (!pix_rst_n) begin
        s_up_frames <= 6'd0;
        s_up_sec    <= 6'd0;
        s_up_min    <= 6'd0;
        s_up_hr     <= 5'd0;
      end else if (s_frame_end) begin
        if (s_up_frames == 6'd59) begin
          s_up_frames <= 6'd0;
          if (s_up_sec == 6'd59) begin
            s_up_sec <= 6'd0;
            if (s_up_min == 6'd59) begin
              s_up_min <= 6'd0;
              s_up_hr  <= (s_up_hr == 5'd23) ? 5'd0 : s_up_hr + 5'd1;
            end else s_up_min <= s_up_min + 6'd1;
          end else s_up_sec <= s_up_sec + 6'd1;
        end else s_up_frames <= s_up_frames + 6'd1;
      end
    end

    term_panel #(
        .FONT_FILE(FONT_FILE),
        .ORIGIN_X (ORIGIN_X),
        .ORIGIN_Y (ORIGIN_Y + ROWS * CELL_H + CELL_H),
        //! Mode 1, in LOGICAL (halved) pixels. The mode-1 text sits at physical
        //! y=40 and is 800 tall, ending at 840 = 420 logical, so the panel goes
        //! just below at 440 logical = 880 physical. Sharing the mode-0 origin
        //! is what pushed the panel off the bottom of a 1080p screen.
        .ORIGIN_X2(160),
        .ORIGIN_Y2(440)
    ) PANEL (
        .clk  (pix_clk),
        .rst_n(pix_rst_n),

        .x     (s_x_raw),
        .y     (s_y_raw),
        .mode  (mode),
        .enable(panel_enable),
        //! Everything the panel draws is latched on this, so a frame renders
        //! from one coherent snapshot instead of from values that move while
        //! the beam is still crossing the glyph.
        .frame_tick(s_frame_end),

        .pil         (w_pil),
        .actlv       (w_actlv),
        .utilization (s_utilization),
        .cache_hit   (s_cache_hit),
        .ring        (w_ring),
        .paging_on   (w_pag),
        .interrupt_on(w_int),
        .running     (w_run),
        .cpu_red     (w_cred),
        .cpu_green   (w_cgrn),
        .hdd_rd      (w_hrd),
        .hdd_wr      (w_hwr),
        .flp_rd      (w_frd),
        .flp_wr      (w_fwr),
        .up_hours    (s_up_hr),
        .up_minutes  (s_up_min),
        .up_seconds  (s_up_sec),
        .mips        (w_mips),

        .active(s_panel_active),
        .colour(s_panel_colour)
    );
    end else begin : g_no_panel
      //! Nothing to draw, and nothing built. The colour mux below then
      //! collapses to the text path alone.
      assign s_panel_active = 1'b0;
      assign s_panel_colour = 3'd0;
    end
  endgenerate

  //! Panel first, then the text. They occupy different rows of the screen, so
  //! the priority never actually arbitrates - it just picks which of the two
  //! is speaking about this pixel.
  assign panel_active = s_panel_active;
  assign colour = s_panel_active ? s_panel_colour
                : s_screen_pixel ? 3'd1 : 3'd0;

  text_screen #(
      .COLS         (COLS),
      .ROWS         (ROWS),
      .CELL_W       (CELL_W),
      .CELL_H       (CELL_H),
      .AWIDTH       (AWIDTH),
`ifdef ND120_TERMINAL_VT100
      .GFX_PAGE     (3),  // DEC Special Graphics (ESC ( 0 + SO/SI), VT100 build
`else
      // TDV2200: cell bit 12 -> PAGE 2, the TDV2200 character set 2. BOTH the
      // ESC 6 (Box) designation and SS2 (ESC N) select it - on a real
      // TDV2200 they are one alphabet. Page 2's glyphs are the real TDV2200
      // ROM's, from RetroCore's dump (font/tdv2200_set2_from_retrocore.py).
      //
      // Page 2 is DELIBERATE, not page 3. On the Nexys, Vivado materialises
      // only the font-ROM pages the design has ever addressed: page 2 is the
      // graphics page every build has used (build 24 rendered DEC diamonds
      // from it), page 3 had never been addressed. Pointing the TDV graphics
      // page at 3 gave a ROM with no page 3 - PED's frames rendered BLANK
      // (02-SEP-2026). Two earlier cuts failed the same family of bug: SS2 on
      // the DEC page (diamonds), then SS2 on a fifth page that pushed the ROM
      // past a block-RAM boundary and was dropped (backticks). Keep the
      // alphabet the Nexys actually draws on the proven page 2.
      .GFX_PAGE     (2),
`endif
      .ORIGIN_X     (ORIGIN_X),
      .ORIGIN_Y     (ORIGIN_Y),
      .FONT_FILE    (FONT_FILE),
      .H_VISIBLE    (H_VISIBLE),
      .H_FRONT_PORCH(H_FRONT_PORCH),
      .H_SYNC       (H_SYNC),
      .H_BACK_PORCH (H_BACK_PORCH),
      .V_VISIBLE    (V_VISIBLE),
      .V_FRONT_PORCH(V_FRONT_PORCH),
      .V_SYNC       (V_SYNC),
      .V_BACK_PORCH (V_BACK_PORCH)
  ) SCREEN (
      .clk  (pix_clk),
      .rst_n(pix_rst_n),

      .ram_raddr(s_raddr),
      .ram_rdata(s_rdata),

      .national     (national),
      .rev_screen   (s_rev_screen),
      .blink_on     (s_blink_on),
      .mode         (mode),

      .top_row      (s_top_row),
      .cursor_col   (s_cursor_col),
      .cursor_row   (s_cursor_row),
      .cursor_enable(s_cursor_enable),

      .pixel    (s_screen_pixel),
      .hsync    (hsync),
      .vsync    (vsync),
      .de       (de),
      .hblank   (hblank),
      .vblank   (vblank),
      .frame_end(s_frame_end),
      .x_raw    (s_x_raw),
      .y_raw    (s_y_raw)
  );

endmodule

`default_nettype wire