Skip to content

Terminal Emulator

A full ANSI terminal emulator with 1280x720 @ 60Hz DVI output, 142×45 character grid with 9×16 pixel cells, SDRAM-backed framebuffer, and compressed font decompression. The host connects via 115200 baud UART; the FPGA renders text, cursor, and ANSI attributes to an HDMI monitor in real time. An EDID reader queries the connected monitor's identity over DDC/I2C.

Clock Tree

text
27 MHz crystal (SCLK)
  └─ PLL (IDIV=3, FBDIV=54, ODIV=2)
       └─ 371.25 MHz serial_clk
            └─ CLKDIV (DIV_MODE=5)
                 └─ 74.25 MHz pixel_clk

PLL math: VCO = 27 × (54+1) × 2 / (3+1) = 742.5 MHz. Base output = 742.5/2 = 371.25 MHz. Pixel clock = 371.25/5 = 74.25 MHz, matching the CEA-861 1280×720@60Hz standard.

Architecture

text
Startup:
  BSRAM (compressed font) → LZSS decompressor → SDRAM

Runtime:
  UART RX → terminal (ANSI parser) → terminal_buffer → SDRAM
  SDRAM → font_cache (scanline buffer) → pixel_generator → TMDS → DVI

  sdram_arb multiplexes 3 clients:
    1. decompressor (highest priority)
    2. terminal_buffer
    3. font_cache

Modules

terminal

Full ANSI terminal emulator. Processes escape sequences (CSI, OSC), manages cursor position, scroll region, character attributes (bold, underline, blink, inverse, strikethrough), 16-color ANSI palette with foreground/background, insert/delete line/character, and alternate screen buffer. Receives bytes from UART RX and drives the terminal buffer with character and attribute writes.

terminal_buffer

142×45 SDRAM-backed double-buffered line cache. Characters and attributes are written by the terminal module and read by the pixel generator, with SDRAM as the backing store. Supports scroll, alternate screen, write-through caching, and read-back for insert mode. Two independent clock-domain ports: sys_clk for CPU writes and pixel_clk for video reads.

pixel_generator

4-cycle pipeline pixel generator. Reads character and attribute data from the terminal buffer, fetches font bitmaps from the font cache, and produces RGB pixel output. Renders a 142×45 grid of 9×16 pixel cells. Supports 16-color ANSI palette with blink, underline, strikethrough, and inverse attributes. Cursor rendering supports 4 styles (underline, block, blinking variants).

font_cache

SDRAM-backed double-buffered font cache. 256×8 BSRAM scanline buffers with character capture and prefetch coordination. Fetches glyph bitmaps from SDRAM (where the decompressor placed them at startup) and serves them to the pixel generator with minimal latency.

lzss_decomp

LZSS + ZeroGlyph decompressor. Decompresses font data from 8 BSRAM banks into SDRAM at startup. Uses a state machine with parity checking and supports a double-layer compression scheme: ZeroGlyph outer layer with LZSS inner layer.

sdram_arb

3-port SDRAM arbiter. Multiplexes the decompressor, terminal buffer, and font cache onto a single SDRAM controller with fixed priority: decompressor (highest, runs only at startup) > terminal buffer > font cache.

sdram

Low-level SDRAM controller for the GW2AR-18's embedded 64 Mbit SDRAM (2M × 32). Initialization: 200µs power-up wait → PRECHARGE ALL → two AUTO REFRESH cycles → MODE REGISTER SET (CL=2, burst=1). Auto-refresh fires every ~7.8µs.

edid_reader

EDID DDC/I2C reader. Reads 128-byte EDID blocks from the connected monitor, parses descriptors for monitor name, serial, and text fields, and extracts native resolution. Implements a full I2C state machine with SDA/SCL open-drain control.

video_timing

CEA-861 compliant 1280×720@60Hz timing generator. 74.25 MHz pixel clock, positive sync polarity. H_ACTIVE=1280, H_TOTAL=1650; V_ACTIVE=720, V_TOTAL=750.

tmds_encoder

DVI TMDS 8b/10b encoder with running disparity tracking for DC balance.

uart_rx / uart_tx

8N1 UART at 115200 baud (74.25 MHz / 644 ≈ 115264 baud). The receiver includes a 2-stage metastability synchronizer and samples at mid-bit.

por

Power-on reset. Counts clock cycles after DONE goes high before releasing active-low por_n.

dvi_top

Top-level integration. Instantiates all submodules and orchestrates the startup sequence (font decompression to SDRAM) followed by runtime operation (UART → terminal → SDRAM → video pipeline → TMDS output).

jz
@project(CHIP="GW2AR-18-QN88-C8-I7") DVI_TEST
    @import "por.jz"
    @import "video_timing.jz"
    @import "tmds_encoder.jz"
    @import "terminal_buffer.jz"
    @import "font_cache.jz"
    @import "lzss_decomp.jz"
    @import "sdram_arb.jz"
    @import "pixel_generator.jz"
    @import "uart_rx.jz"
    @import "uart_tx.jz"
    @import "terminal.jz"
    @import "edid_reader.jz"
    @import "sdram.jz"
    @import "dvi.jz"

    CLOCKS {
        SCLK       = { period=37.037 }; // 27MHz crystal
        serial_clk;                      // 371.25MHz (5x pixel, from PLL)
        pixel_clk;                       // 74.25MHz (from CLKDIV)
    }

    IN_PINS {
        SCLK     = { standard=LVCMOS33 };
        DONE     = { standard=LVCMOS33 };
        KEY[2]   = { standard=LVCMOS33 };
        UART_RX  = { standard=LVCMOS33 };
    }

    OUT_PINS {
        LED[6]       = { standard=LVCMOS33, drive=8 };
        TMDS_CLK     = { mode=DIFFERENTIAL, standard=LVDS25, drive=3.5, width=10, fclk=serial_clk, pclk=pixel_clk, reset=pll_lock };
        TMDS_DATA[3] = { mode=DIFFERENTIAL, standard=LVDS25, drive=3.5, width=10, fclk=serial_clk, pclk=pixel_clk, reset=pll_lock };
        UART_TX      = { standard=LVCMOS33, drive=8 };
        EDID_CLK     = { standard=LVCMOS33, drive=4 };

        O_sdram_clk      = { standard=LVCMOS33, drive=8 };
        O_sdram_cke      = { standard=LVCMOS33, drive=8 };
        O_sdram_cs_n     = { standard=LVCMOS33, drive=8 };
        O_sdram_cas_n    = { standard=LVCMOS33, drive=8 };
        O_sdram_ras_n    = { standard=LVCMOS33, drive=8 };
        O_sdram_wen_n    = { standard=LVCMOS33, drive=8 };
        O_sdram_dqm[4]   = { standard=LVCMOS33, drive=8 };
        O_sdram_addr[11] = { standard=LVCMOS33, drive=8 };
        O_sdram_ba[2]    = { standard=LVCMOS33, drive=8 };
    }

    INOUT_PINS {
        EDID_DAT = { standard=LVCMOS33, drive=4 };
        IO_sdram_dq[32] = { standard=LVCMOS33, drive=8 };
    }

    MAP {
        // System Clock 27MHz
        SCLK = 4;

        // Buttons (active high)
        KEY[0] = 87;
        KEY[1] = 88;

        // LEDs (active low)
        LED[0] = 15;
        LED[1] = 16;
        LED[2] = 17;
        LED[3] = 18;
        LED[4] = 19;
        LED[5] = 20;

        // DVI TMDS differential pairs
        TMDS_CLK     = { P=33, N=34 };
        TMDS_DATA[0] = { P=35, N=36 };
        TMDS_DATA[1] = { P=37, N=38 };
        TMDS_DATA[2] = { P=39, N=40 };

        // UART RX/TX (directly from USB-C serial)
        UART_RX = 70;
        UART_TX = 69;

        // SDRAM
        O_sdram_clk      = IOR11B;
        O_sdram_cke      = IOL13A;
        O_sdram_cs_n     = IOL14B;
        O_sdram_cas_n    = IOL14A;
        O_sdram_ras_n    = IOL13B;
        O_sdram_wen_n    = IOL12B;
        O_sdram_dqm[0]   = IOL12A;
        O_sdram_dqm[1]   = IOR11A;
        O_sdram_dqm[2]   = IOL18A;
        O_sdram_dqm[3]   = IOR15B;
        O_sdram_addr[0]  = IOR14A;
        O_sdram_addr[1]  = IOR13B;
        O_sdram_addr[2]  = IOR14B;
        O_sdram_addr[3]  = IOR15A;
        O_sdram_addr[4]  = IOL16B;
        O_sdram_addr[5]  = IOL17B;
        O_sdram_addr[6]  = IOL16A;
        O_sdram_addr[7]  = IOL17A;
        O_sdram_addr[8]  = IOL15B;
        O_sdram_addr[9]  = IOL15A;
        O_sdram_addr[10] = IOR12B;
        O_sdram_ba[0]    = IOR13A;
        O_sdram_ba[1]    = IOR12A;
        IO_sdram_dq[0]   = IOL3A;
        IO_sdram_dq[1]   = IOL3B;
        IO_sdram_dq[2]   = IOL8A;
        IO_sdram_dq[3]   = IOL8B;
        IO_sdram_dq[4]   = IOL9A;
        IO_sdram_dq[5]   = IOL9B;
        IO_sdram_dq[6]   = IOL11A;
        IO_sdram_dq[7]   = IOL11B;
        IO_sdram_dq[8]   = IOR9B;
        IO_sdram_dq[9]   = IOR9A;
        IO_sdram_dq[10]  = IOR5B;
        IO_sdram_dq[11]  = IOR6A;
        IO_sdram_dq[12]  = IOR5A;
        IO_sdram_dq[13]  = IOR4B;
        IO_sdram_dq[14]  = IOR3B;
        IO_sdram_dq[15]  = IOR3A;
        IO_sdram_dq[16]  = IOL39B;
        IO_sdram_dq[17]  = IOL39A;
        IO_sdram_dq[18]  = IOL35B;
        IO_sdram_dq[19]  = IOL35A;
        IO_sdram_dq[20]  = IOL30B;
        IO_sdram_dq[21]  = IOL30A;
        IO_sdram_dq[22]  = IOL20A;
        IO_sdram_dq[23]  = IOL18B;
        IO_sdram_dq[24]  = IOR17A;
        IO_sdram_dq[25]  = IOR16A;
        IO_sdram_dq[26]  = IOR16B;
        IO_sdram_dq[27]  = IOR17B;
        IO_sdram_dq[28]  = IOR18A;
        IO_sdram_dq[29]  = IOR18B;
        IO_sdram_dq[30]  = IOR44A;
        IO_sdram_dq[31]  = IOR44B;

        // DONE (POR)
        DONE = IOR32B;

        // EDID DDC (I2C on DVI connector)
        EDID_DAT = 52;
        EDID_CLK = 53;
    }

    CLOCK_GEN {
        PLL {
            IN REF_CLK SCLK;
            OUT BASE   serial_clk;  // 371.25 MHz (5x pixel clock)
            WIRE LOCK  pll_lock;

            CONFIG {
                IDIV = 3;           // divider = 4
                FBDIV = 54;         // multiplier = 55
                ODIV = 2;           // VCO = 371.25 * 2 = 742.5 MHz
            };
        };
        CLKDIV {
            IN REF_CLK serial_clk;
            OUT BASE  pixel_clk;   // 371.25 / 5 = 74.25 MHz

            CONFIG {
                DIV_MODE = 5;
            };
        };
    }

    @top dvi_top {
        IN   [1]  clk       = pixel_clk;
        IN   [1]  por       = DONE;
        IN   [1]  rst_n     = ~KEY[0];
        IN   [1]  uart_rx   = UART_RX;
        OUT  [6]  leds      = LED;
        OUT  [10] tmds_clk  = TMDS_CLK;
        OUT  [10] tmds_d0   = TMDS_DATA[0];
        OUT  [10] tmds_d1   = TMDS_DATA[1];
        OUT  [10] tmds_d2   = TMDS_DATA[2];
        OUT  [1]  uart_tx   = UART_TX;
        INOUT [1] edid_sda  = EDID_DAT;
        OUT  [1]  edid_scl  = EDID_CLK;
        OUT  [1]  sdram_cke    = O_sdram_cke;
        OUT  [1]  sdram_cs_n   = O_sdram_cs_n;
        OUT  [1]  sdram_ras_n  = O_sdram_ras_n;
        OUT  [1]  sdram_cas_n  = O_sdram_cas_n;
        OUT  [1]  sdram_wen_n  = O_sdram_wen_n;
        OUT  [4]  sdram_dqm    = O_sdram_dqm;
        OUT  [11] sdram_addr   = O_sdram_addr;
        OUT  [2]  sdram_ba     = O_sdram_ba;
        INOUT [32] sdram_dq    = IO_sdram_dq;
        OUT  [1]  sdram_clk_out = O_sdram_clk;
    }
@endproj
jz
// DVI 1280x720 @ 60Hz Text-Mode Terminal Display
// 142x45 character grid with 9x16 pixel cells, TMDS encoding for DVI output.
//
// Architecture:
//   Startup: BSRAM (compressed font) -> LZSS decompressor -> SDRAM
//   Runtime: SDRAM -> font cache (scanline buffer) -> pixel generator
//            SDRAM -> terminal buffer (line cache) -> pixel generator
//   SDRAM arbiter multiplexes 3 clients: terminal buffer, font cache, decompressor
@module dvi_top
    PORT {
        IN   [1]  clk;         // pixel_clk (74.25 MHz)
        IN   [1]  por;         // POR input from DONE
        IN   [1]  rst_n;       // Active-low reset from button
        IN   [1]  uart_rx;     // UART receive pin
        OUT  [6]  leds;        // Status LEDs
        OUT  [10] tmds_clk;    // TMDS clock channel (serialized)
        OUT  [10] tmds_d0;     // TMDS data channel 0 (blue)
        OUT  [10] tmds_d1;     // TMDS data channel 1 (green)
        OUT  [10] tmds_d2;     // TMDS data channel 2 (red)
        OUT  [1]  uart_tx;     // UART transmit pin
        INOUT [1] edid_sda;    // DDC data line (I2C)
        OUT  [1]  edid_scl;    // DDC clock line (I2C)

        // SDRAM physical interface (from arbiter)
        OUT  [1]  sdram_cke;
        OUT  [1]  sdram_cs_n;
        OUT  [1]  sdram_ras_n;
        OUT  [1]  sdram_cas_n;
        OUT  [1]  sdram_wen_n;
        OUT  [4]  sdram_dqm;
        OUT  [11] sdram_addr;
        OUT  [2]  sdram_ba;
        INOUT [32] sdram_dq;
        OUT  [1]  sdram_clk_out;
    }

    WIRE {
        reset       [1];
        por_n       [1];
        hsync       [1];
        vsync       [1];
        de          [1];
        x_pos       [11];
        y_pos       [10];
        red         [8];
        green       [8];
        blue        [8];
        pg_de       [1];
        pg_hsync    [1];
        pg_vsync    [1];

        // UART -> RX buffer (raw from uart_rx)
        rx_raw_data  [8];
        rx_raw_valid [1];

        // RX buffer -> Terminal
        rx_data     [8];
        rx_valid    [1];
        data_ack    [1];

        // Terminal -> Terminal Buffer
        wr_row      [6];
        wr_col      [8];
        wr_char     [16];
        wr_attr     [16];
        wr_en       [1];

        // Terminal -> Terminal Buffer (read-back for insert mode)
        tb_rd_row   [6];
        tb_rd_col   [8];
        tb_rd_char  [16];
        tb_rd_attr  [16];
        tb_rd_en    [1];

        // Terminal ANSI command buffer (active but unprocessed for now)
        cmd_ready   [1];
        cmd_final   [8];
        cmd_len     [7];
        cmd_rd_data [8];

        // Terminal -> Terminal Buffer (scroll)
        scroll       [1];
        scroll_down  [1];
        scroll_reset [1];
        buf_busy     [1];

        // Terminal -> Terminal Buffer (alt screen)
        use_alt      [1];

        // UART TX
        tx_data      [8];
        tx_valid     [1];
        tx_ready     [1];
        uart_tx_pin  [1];

        // Pixel Generator <-> Terminal Buffer
        rd_row      [6];
        rd_col      [8];
        rd_row_next [6];
        buf_char    [16];
        buf_attr    [16];
        buf_ready   [1];

        // EDID reader interface
        edid_field     [2];
        edid_byte_idx  [4];
        edid_byte      [8];
        edid_ready     [1];
        edid_error     [1];

        // DVI TMDS encoder outputs (active during video)
        dvi_tmds_d0 [10];
        dvi_tmds_d1 [10];
        dvi_tmds_d2 [10];

        display_enabled [1];

        // Terminal buffer <-> SDRAM arbiter (port 0)
        tb_sd_addr   [21];
        tb_sd_wdata  [32];
        tb_sd_rd     [1];
        tb_sd_wr     [1];
        tb_sd_rdata  [32];
        tb_sd_busy   [1];
        tb_sd_done   [1];

        // Font cache <-> SDRAM arbiter (port 1)
        fc_sd_addr   [21];
        fc_sd_rd     [1];
        fc_sd_rdata  [32];
        fc_sd_busy   [1];
        fc_sd_done   [1];

        // Decompressor <-> SDRAM arbiter (port 2)
        dc_sd_addr   [21];
        dc_sd_wdata  [32];
        dc_sd_wr     [1];
        dc_sd_busy   [1];
        dc_sd_done   [1];

        // Decompressor control
        decomp_done  [1];
        decomp_start [1];
    }

    REGISTER {
        heartbeat_cnt [25] = 25'b0;
        heartbeat_led [1]  = 1'b0;

        // 16-byte RX FIFO (holds UART bytes until terminal consumes them)
        fifo_wr_ptr [4] = 4'd0;
        fifo_rd_ptr [4] = 4'd0;
        fifo_count  [5] = 5'd0;

        // LED activity stretch counters (make short pulses visible)
        rx_led_cnt [20] = 20'd0;
        tx_led_cnt [20] = 20'd0;

        // Startup sequencing: pulse start to decompressor after reset
        startup_cnt [2] = 2'd0;
    }

    MEM(TYPE=DISTRIBUTED) {
        rx_fifo [8] [16] = 8'd0 { OUT rd ASYNC; IN wr; };
    }

    @new por0 por {
        IN  [1] clk   = clk;
        IN  [1] done  = por;
        OUT [1] por_n = por_n;
    }

    @new vt0 video_timing {
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = reset;
        OUT [1]  hsync          = hsync;
        OUT [1]  vsync          = vsync;
        OUT [1]  display_enable = de;
        OUT [11] x_pos          = x_pos;
        OUT [10] y_pos          = y_pos;
    }

    @new urx0 uart_rx {
        IN  [1] clk   = clk;
        IN  [1] rst_n = reset;
        IN  [1] rx    = uart_rx;
        OUT [8] data  = rx_raw_data;
        OUT [1] valid = rx_raw_valid;
    }

    @new term0 terminal {
        IN  [1]  clk         = clk;
        IN  [1]  rst_n       = reset;
        IN  [8]  data        = rx_data;
        IN  [1]  valid       = rx_valid;
        OUT [1]  data_ack    = data_ack;
        OUT [6]  wr_row      = wr_row;
        OUT [8]  wr_col      = wr_col;
        OUT [16] wr_char     = wr_char;
        OUT [16] wr_attr     = wr_attr;
        OUT [1]  wr_en       = wr_en;
        OUT [6]  tb_rd_row   = tb_rd_row;
        OUT [8]  tb_rd_col   = tb_rd_col;
        IN  [16] tb_rd_char  = tb_rd_char;
        IN  [16] tb_rd_attr  = tb_rd_attr;
        OUT [1]  tb_rd_en    = tb_rd_en;
        OUT [1]  cmd_ready   = cmd_ready;
        OUT [8]  cmd_final   = cmd_final;
        OUT [7]  cmd_len     = cmd_len;
        IN  [7]  cmd_rd_addr = 7'd0;
        OUT [8]  cmd_rd_data = cmd_rd_data;
        OUT [1]  scroll       = scroll;
        OUT [1]  scroll_down  = scroll_down;
        OUT [1]  scroll_reset = scroll_reset;
        IN  [1]  busy         = buf_busy;
        OUT [8]  tx_data      = tx_data;
        OUT [1]  tx_valid     = tx_valid;
        IN  [1]  tx_ready     = tx_ready;
        OUT [2]  edid_field    = edid_field;
        OUT [4]  edid_byte_idx = edid_byte_idx;
        IN  [8]  edid_byte     = edid_byte;
        IN  [1]  edid_ready    = edid_ready;
        IN  [1]  edid_error    = edid_error;
        OUT [1]  use_alt       = use_alt;
    }

    @new tbuf0 terminal_buffer {
        IN  [1]  clk      = clk;
        IN  [1]  rst_n    = reset;
        IN  [6]  rd_row   = rd_row;
        IN  [8]  rd_col   = rd_col;
        OUT [16] char_code = buf_char;
        OUT [16] attr_out  = buf_attr;
        IN  [6]  wr_row   = wr_row;
        IN  [8]  wr_col   = wr_col;
        IN  [16] wr_char  = wr_char;
        IN  [16] wr_attr  = wr_attr;
        IN  [1]  wr_en    = wr_en;
        IN  [6]  tb_rd_row  = tb_rd_row;
        IN  [8]  tb_rd_col  = tb_rd_col;
        OUT [16] tb_rd_char = tb_rd_char;
        OUT [16] tb_rd_attr = tb_rd_attr;
        IN  [1]  tb_rd_en   = tb_rd_en;
        IN  [1]  scroll       = scroll;
        IN  [1]  scroll_down  = scroll_down;
        IN  [1]  scroll_reset = scroll_reset;
        IN  [1]  use_alt      = use_alt;
        OUT [1]  busy         = buf_busy;
        IN  [6]  rd_row_next  = rd_row_next;
        IN  [1]  display_active = de;
        OUT [1]  buf_ready    = buf_ready;
        OUT [21] sd_addr      = tb_sd_addr;
        OUT [32] sd_wdata     = tb_sd_wdata;
        OUT [1]  sd_rd        = tb_sd_rd;
        OUT [1]  sd_wr        = tb_sd_wr;
        IN  [32] sd_rdata     = tb_sd_rdata;
        IN  [1]  sd_busy      = tb_sd_busy;
        IN  [1]  sd_done      = tb_sd_done;
    }

    @new pg0 pixel_generator {
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = reset;
        IN  [11] x_pos          = x_pos;
        IN  [10] y_pos          = y_pos;
        IN  [1]  display_enable = de;
        IN  [1]  hsync          = hsync;
        IN  [1]  vsync          = vsync;
        OUT [6]  rd_row         = rd_row;
        OUT [8]  rd_col         = rd_col;
        IN  [16] buf_char       = buf_char;
        IN  [16] buf_attr       = buf_attr;
        IN  [1]  buf_ready      = buf_ready;
        OUT [6]  rd_row_next    = rd_row_next;
        OUT [8]  red            = red;
        OUT [8]  green          = green;
        OUT [8]  blue           = blue;
        OUT [1]  de_out         = pg_de;
        OUT [1]  hsync_out      = pg_hsync;
        OUT [1]  vsync_out      = pg_vsync;
        OUT [21] font_sd_addr   = fc_sd_addr;
        OUT [1]  font_sd_rd     = fc_sd_rd;
        IN  [32] font_sd_rdata  = fc_sd_rdata;
        IN  [1]  font_sd_busy   = fc_sd_busy;
        IN  [1]  font_sd_done   = fc_sd_done;
        IN  [1]  font_ready     = decomp_done;
    }

    // LZSS Decompressor: reads compressed font from BSRAM, writes to SDRAM at startup
    @new decomp0 lzss_decomp {
        IN  [1]  clk     = clk;
        IN  [1]  rst_n   = reset;
        IN  [1]  start   = decomp_start;
        OUT [1]  done    = decomp_done;
        OUT [21] sd_addr = dc_sd_addr;
        OUT [32] sd_wdata = dc_sd_wdata;
        OUT [1]  sd_wr    = dc_sd_wr;
        IN  [1]  sd_busy  = dc_sd_busy;
        IN  [1]  sd_done  = dc_sd_done;
    }

    // SDRAM Arbiter: 3-port mux with fixed priority
    @new arb0 sdram_arb {
        IN  [1]  clk     = clk;
        IN  [1]  rst_n   = reset;
        // Port 0: Terminal buffer
        IN  [21] p0_addr  = tb_sd_addr;
        IN  [32] p0_wdata = tb_sd_wdata;
        IN  [1]  p0_rd    = tb_sd_rd;
        IN  [1]  p0_wr    = tb_sd_wr;
        OUT [32] p0_rdata = tb_sd_rdata;
        OUT [1]  p0_busy  = tb_sd_busy;
        OUT [1]  p0_done  = tb_sd_done;
        // Port 1: Font cache
        IN  [21] p1_addr  = fc_sd_addr;
        IN  [1]  p1_rd    = fc_sd_rd;
        OUT [32] p1_rdata = fc_sd_rdata;
        OUT [1]  p1_busy  = fc_sd_busy;
        OUT [1]  p1_done  = fc_sd_done;
        // Port 2: Decompressor
        IN  [21] p2_addr  = dc_sd_addr;
        IN  [32] p2_wdata = dc_sd_wdata;
        IN  [1]  p2_wr    = dc_sd_wr;
        OUT [1]  p2_busy  = dc_sd_busy;
        OUT [1]  p2_done  = dc_sd_done;
        // SDRAM physical pins
        OUT   [1]  sdram_cke   = sdram_cke;
        OUT   [1]  sdram_cs_n  = sdram_cs_n;
        OUT   [1]  sdram_ras_n = sdram_ras_n;
        OUT   [1]  sdram_cas_n = sdram_cas_n;
        OUT   [1]  sdram_wen_n = sdram_wen_n;
        OUT   [4]  sdram_dqm   = sdram_dqm;
        OUT   [11] sdram_addr  = sdram_addr;
        OUT   [2]  sdram_ba    = sdram_ba;
        INOUT [32] sdram_dq    = sdram_dq;
    }

    // Blue channel (data channel 0) - carries sync signals
    // During preamble: ch0 gets normal hsync/vsync, ch1 gets CTL(1,0), ch2 gets CTL(1,0)
    @new enc0 tmds_encoder {
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = reset;
        IN  [8]  data_in        = blue;
        IN  [1]  c0             = pg_hsync;
        IN  [1]  c1             = pg_vsync;
        IN  [1]  display_enable = display_enabled;
        OUT [10] tmds_out       = dvi_tmds_d0;
    }

    // Green channel (data channel 1)
    @new enc1 tmds_encoder {
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = reset;
        IN  [8]  data_in        = green;
        IN  [1]  c0             = 1'b0;
        IN  [1]  c1             = 1'b0;
        IN  [1]  display_enable = display_enabled;
        OUT [10] tmds_out       = dvi_tmds_d1;
    }

    // Red channel (data channel 2)
    @new enc2 tmds_encoder {
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = reset;
        IN  [8]  data_in        = red;
        IN  [1]  c0             = 1'b0;
        IN  [1]  c1             = 1'b0;
        IN  [1]  display_enable = display_enabled;
        OUT [10] tmds_out       = dvi_tmds_d2;
    }

    @new utx0 uart_tx {
        IN  [1] clk   = clk;
        IN  [1] rst_n = reset;
        IN  [8] data  = tx_data;
        IN  [1] valid = tx_valid;
        OUT [1] ready = tx_ready;
        OUT [1] tx    = uart_tx_pin;
    }

    @new edid0 edid_reader {
        IN    [1]  clk      = clk;
        IN    [1]  rst_n    = reset;
        INOUT [1]  sda      = edid_sda;
        OUT   [1]  scl      = edid_scl;
        IN    [2]  field    = edid_field;
        IN    [4]  byte_idx = edid_byte_idx;
        OUT   [8]  byte_out = edid_byte;
        OUT   [1]  ready    = edid_ready;
        OUT   [1]  error    = edid_error;
    }

    ASYNCHRONOUS {
        reset <= rst_n & por_n;
        uart_tx <= uart_tx_pin;

        display_enabled = pg_de;

        // Startup: pulse decomp_start a few cycles after reset
        IF (startup_cnt == 2'd2 && decomp_done == 1'b0) {
            decomp_start <= 1'b1;
        } ELSE {
            decomp_start <= 1'b0;
        }

        // RX FIFO -> terminal
        rx_data  <= rx_fifo.rd[fifo_rd_ptr];
        IF (fifo_count != 5'd0) {
            rx_valid <= 1'b1;
        } ELSE {
            rx_valid <= 1'b0;
        }

        // DVI TMDS output (direct from encoders)
        tmds_d0 <= dvi_tmds_d0;
        tmds_d1 <= dvi_tmds_d1;
        tmds_d2 <= dvi_tmds_d2;

        // TMDS clock channel: fixed pattern for pixel clock recovery
        tmds_clk <= 10'b1111100000;

        // SDRAM clock: drive from pixel clock (active high)
        sdram_clk_out <= ~clk;

        // LED status: heartbeat, RX activity, TX activity
        IF (rx_led_cnt != 20'd0) {
            leds[0] <= 1'b0;
        } ELSE {
            leds[0] <= 1'b1;
        }
        IF (tx_led_cnt != 20'd0) {
            leds[1] <= 1'b0;
        } ELSE {
            leds[1] <= 1'b1;
        }
        leds[2] <= ~heartbeat_led;
        leds[3] <= 1'b1;
        leds[4] <= 1'b1;
        leds[5] <= 1'b1;
    }

    SYNCHRONOUS(CLK=clk RESET=reset RESET_ACTIVE=Low) {
        // Heartbeat blinker
        IF (heartbeat_cnt == 25'd33_554_431) {
            heartbeat_cnt <= 25'b0;
            heartbeat_led <= ~heartbeat_led;
        } ELSE {
            heartbeat_cnt <= heartbeat_cnt + 25'b1;
        }

        // RX/TX LED stretch: reload on activity, count down to zero
        IF (rx_raw_valid == 1'b1) {
            rx_led_cnt <= 20'hFFFFF;
        } ELIF (rx_led_cnt != 20'd0) {
            rx_led_cnt <= rx_led_cnt - 20'd1;
        }

        IF (tx_valid == 1'b1) {
            tx_led_cnt <= 20'hFFFFF;
        } ELIF (tx_led_cnt != 20'd0) {
            tx_led_cnt <= tx_led_cnt - 20'd1;
        }

        // Startup counter: count up to 2 then hold
        IF (startup_cnt != 2'd3) {
            startup_cnt <= startup_cnt + 2'd1;
        }

        // 16-byte RX FIFO: enqueue from UART, dequeue on terminal ack
        IF (rx_raw_valid == 1'b1 && fifo_count != 5'd16) {
            rx_fifo.wr[fifo_wr_ptr] <= rx_raw_data;
            fifo_wr_ptr <= fifo_wr_ptr + 4'd1;
        }

        IF (data_ack == 1'b1 && fifo_count != 5'd0) {
            fifo_rd_ptr <= fifo_rd_ptr + 4'd1;
        }

        // Update count: simultaneous enqueue+dequeue keeps count unchanged
        IF (rx_raw_valid == 1'b1 && fifo_count != 5'd16 && data_ack == 1'b1 && fifo_count != 5'd0) {
            fifo_count <= fifo_count;
        } ELIF (rx_raw_valid == 1'b1 && fifo_count != 5'd16) {
            fifo_count <= fifo_count + 5'd1;
        } ELIF (data_ack == 1'b1 && fifo_count != 5'd0) {
            fifo_count <= fifo_count - 5'd1;
        }
    }
@endmod
jz
// Terminal controller — writes incoming characters to terminal buffer
// Maintains a cursor (row, col). Each received byte is written at the
// cursor position, then col increments. At end of line, wraps to next row.
//
// ANSI CSI parser: ESC [ <params> <final>
//   - ESC (0x1B) followed by '[' enters CSI mode
//   - ESC followed by non-'[' emits both ESC and that character
//   - In CSI mode, digits (0-9), ';', '>', '?' are collected into cmd_buf
//   - Any other byte is the final command byte; cmd_ready pulses
//   - Buffer overflow (>80 param bytes) aborts the sequence
//
// SGR (Select Graphic Rendition) — ESC [ <params> m
//   - 0 = reset all attributes and colors to default
//   - 1 = bold (maps to bright color: fg_idx |= 8)
//   - 4 = underline, 5 = slow blink, 6 = fast blink
//   - 7 = inverse, 8 = conceal, 9 = strikethrough
//   - 24 = no underline, 25 = no blink, 27 = no inverse
//   - 28 = no conceal, 29 = no strikethrough
//   - 30-37 = FG standard, 40-47 = BG standard
//   - 90-97 = FG bright, 100-107 = BG bright
//
// Attribute word layout (16 bits, stored in terminal_buffer):
//   [15:12] = FG color index (0-15)
//   [11:8]  = BG color index (0-15)
//   [7]     = underline
//   [6]     = slow_blink
//   [5]     = fast_blink
//   [4]     = strikethrough
//   [3:0]   = reserved
//
// Mode toggles (CSI ? h / CSI ? l):
//   ?1  = application cursor keys
//   ?7  = autowrap
//   ?25 = cursor visible
//   CSI 4 h/l = insert mode
//   CSI 20 h/l = newline mode
//
// DCS (Device Control String):
//   ESC P <content> ESC \
//   EDID query: ESC P EDID ESC \
//     Response: ESC P <n> EDID=<data> ESC \  (n=1..4)
//   TEST1: ESC P TEST1 ESC \  (draws ruler pattern)
@module terminal
    CONST {
        MAX_COL = 141;
        MAX_ROW = 44;
        CMD_BUF_MAX = 79;

        // States (5 bits for 20 states)
        ST_NORMAL       = 0;
        ST_ESC          = 1;
        ST_CSI          = 2;
        ST_ESC_EMIT     = 3;
        ST_CSI_COMMIT   = 4;
        ST_CLEAR        = 5;
        ST_SCROLL_MULTI = 6;
        ST_RESP_COMPUTE = 7;
        ST_RESP_SEND    = 8;
        ST_DCS          = 9;
        ST_DCS_ESC      = 10;
        ST_EDID_FETCH   = 11;
        ST_ESC_LPAREN   = 12;
        ST_INSERT_READ  = 13;
        ST_INSERT_WRITE = 14;
        ST_TEST_DRAW    = 15;
        ST_UTF8         = 16;
        ST_OSC          = 17;
        ST_OSC_ESC      = 18;
        ST_ESC_HASH     = 19;
        ST_WIDE_RIGHT   = 20;
        ST_WIDE_WRAP    = 21;
        ST_KANJI_READ   = 22;   // Binary search: read kanji_lut[mid]
        ST_KANJI_CMP    = 23;   // Binary search: compare and narrow range
        ST_KANJI_DONE   = 24;   // Search complete, proceed to wide char write
        // ASCII
        CHAR_LF        = 10;
        CHAR_CR        = 13;
        CHAR_TAB       = 9;
        CHAR_ESC       = 27;
        CHAR_DEL       = 127;
        CHAR_LBRACKET  = 91;
        CHAR_P         = 80;
        CHAR_BACKSLASH = 92;
        CHAR_LPAREN    = 40;
        CHAR_0        = 48;
        CHAR_9        = 57;
        CHAR_SEMI     = 59;
        CHAR_GT       = 62;
        CHAR_QUESTION = 63;
        CHAR_A        = 65;
        CHAR_B_upper  = 66;
        CHAR_C        = 67;
        CHAR_D        = 68;
        CHAR_E        = 69;
        CHAR_F        = 70;
        CHAR_G        = 71;
        CHAR_H        = 72;
        CHAR_J        = 74;
        CHAR_K        = 75;
        CHAR_R        = 82;
        CHAR_S        = 83;
        CHAR_T        = 84;
        CHAR_U_upper  = 85;
        CHAR_RBRACKET = 93;
        CHAR_EQUALS   = 61;
        CHAR_c_lower  = 99;
        CHAR_HASH     = 35;
        CHAR_BEL      = 7;
        CHAR_M        = 77;
        CHAR_7        = 55;
        CHAR_8        = 56;
        CHAR_h        = 104;
        CHAR_l        = 108;
        CHAR_m        = 109;
        CHAR_n        = 110;
        CHAR_o_lower  = 111;
        CHAR_p_lower  = 112;
        CHAR_q        = 113;
        CHAR_s_lower  = 115;
        CHAR_t_lower  = 116;
        CHAR_u        = 117;

    }

    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;

        // From UART receiver
        IN  [8]  data;
        IN  [1]  valid;

        // Write interface to terminal_buffer
        OUT [6]  wr_row;
        OUT [8]  wr_col;
        OUT [16] wr_char;
        OUT [16] wr_attr;
        OUT [1]  wr_en;

        // Terminal buffer read-back (for insert mode)
        OUT [6]  tb_rd_row;
        OUT [8]  tb_rd_col;
        IN  [16] tb_rd_char;
        IN  [16] tb_rd_attr;
        OUT [1]  tb_rd_en;

        // Command buffer status
        OUT [1]  cmd_ready;
        OUT [8]  cmd_final;
        OUT [7]  cmd_len;

        // Command buffer read interface
        IN  [7]  cmd_rd_addr;
        OUT [8]  cmd_rd_data;

        // Scroll interface
        OUT [1]  scroll;
        OUT [1]  scroll_down;
        IN  [1]  busy;

        // Scroll reset (for CSI 3 J)
        OUT [1]  scroll_reset;

        // UART TX interface (for CPR and version responses)
        OUT [8]  tx_data;
        OUT [1]  tx_valid;
        IN  [1]  tx_ready;

        // EDID reader interface
        OUT [2]  edid_field;
        OUT [4]  edid_byte_idx;
        IN  [8]  edid_byte;
        IN  [1]  edid_ready;
        IN  [1]  edid_error;

        // Alt screen active
        OUT [1]  use_alt;

        // Data acknowledge (pulses when byte consumed from valid/data)
        OUT [1]  data_ack;

    }

    WIRE {
        param_x10      [8];
        dcs_expected   [8];
        edid_data_idx  [5];
        wr_attr_word   [16];
        actual_fg      [4];
        actual_bg      [4];

        // Kanji binary search midpoint
        search_mid_sum [9];
        search_mid     [8];
    }

    REGISTER {
        cursor_row  [6]  = 6'd0;
        cursor_col  [8]  = 8'd0;

        // Registered write outputs (hold for 1 cycle)
        do_write    [1]  = 1'b0;
        do_scroll   [1]  = 1'b0;
        write_row   [6]  = 6'd0;
        write_col   [8]  = 8'd0;
        write_char  [16] = 16'h0000;
        write_attr  [16] = 16'h0000;
        use_write_attr [1] = 1'b0;

        // State machine (5 bits for 16 states)
        state       [5]  = 5'b00000;
        buf_ptr     [7]  = 7'd0;
        saved_char  [8]  = 8'h00;

        // Command output registers
        cmd_ready_r [1]  = 1'b0;
        cmd_final_r [8]  = 8'h00;
        cmd_len_r   [7]  = 7'd0;

        // Command buffer read output
        cmd_rd_out  [8]  = 8'h00;

        // Color index registers (4-bit ANSI palette indices)
        fg_idx      [4]  = 4'd7;
        bg_idx      [4]  = 4'd0;

        // Pending colors (updated during CSI, committed on 'm')
        pend_fg_idx [4]  = 4'd7;
        pend_bg_idx [4]  = 4'd0;

        // SGR attribute flags
        attr_underline    [1] = 1'b0;
        attr_slow_blink   [1] = 1'b0;
        attr_fast_blink   [1] = 1'b0;
        attr_inverse      [1] = 1'b0;
        attr_conceal      [1] = 1'b0;
        attr_strikethrough [1] = 1'b0;

        // Pending SGR attribute flags
        pend_underline    [1] = 1'b0;
        pend_slow_blink   [1] = 1'b0;
        pend_fast_blink   [1] = 1'b0;
        pend_inverse      [1] = 1'b0;
        pend_conceal      [1] = 1'b0;
        pend_strikethrough [1] = 1'b0;

        // Inline decimal parameter accumulator
        param_val   [8]  = 8'h00;

        // CSI parameter tracking
        param1      [8]  = 8'd0;
        has_semi    [1]  = 1'b0;

        // CSI prefix flags
        csi_gt       [1] = 1'b0;
        csi_question [1] = 1'b0;

        // Screen clear state
        clr_row     [6]  = 6'd0;
        clr_col     [8]  = 8'd0;
        clr_end_row [6]  = 6'd0;
        clr_end_col [8]  = 8'd0;

        // Scroll reset pulse
        do_scroll_reset [1] = 1'b0;

        // Save/restore cursor (includes color + attributes)
        saved_cursor_row [6] = 6'd0;
        saved_cursor_col [8] = 8'd0;
        saved_fg_idx     [4] = 4'd7;
        saved_bg_idx     [4] = 4'd0;
        saved_underline    [1] = 1'b0;
        saved_slow_blink   [1] = 1'b0;
        saved_fast_blink   [1] = 1'b0;
        saved_inverse      [1] = 1'b0;
        saved_conceal      [1] = 1'b0;
        saved_strikethrough [1] = 1'b0;

        // Alt screen buffer state
        use_alt_r      [1] = 1'b0;
        alt_cursor_row [6] = 6'd0;
        alt_cursor_col [8] = 8'd0;
        param_overflow [1] = 1'b0;

        // Multi-line scroll state
        scroll_count [8] = 8'd0;
        scroll_dir   [1] = 1'b0;
        do_scroll_down [1] = 1'b0;

        // UART TX state
        do_tx      [1] = 1'b0;
        tx_byte_r  [8] = 8'd0;

        // Response state (CPR / version)
        resp_type  [2] = 2'd0;
        resp_idx   [5] = 5'd0;
        resp_len   [5] = 5'd0;

        // DCS parser state
        dcs_match_idx  [4] = 4'd0;
        dcs_matched    [1] = 1'b0;
        dcs_test_matched [1] = 1'b0;

        // EDID response phase (0-3)
        edid_phase [2] = 2'd0;

        // EDID reader interface registers
        edid_field_r    [2] = 2'd0;
        edid_byte_idx_r [4] = 4'd0;
        edid_wait       [2] = 2'd0;

        // CPR BCD digits
        cpr_row    [7] = 7'd0;
        cpr_col    [8] = 8'd0;
        cpr_row_tens  [4] = 4'd0;
        cpr_row_ones  [7] = 7'd0;
        cpr_col_hund  [4] = 4'd0;
        cpr_col_tens  [8] = 8'd0;
        cpr_col_ones  [8] = 8'd0;

        // Mode flags
        newline_mode  [1] = 1'b0;
        insert_mode   [1] = 1'b0;
        autowrap      [1] = 1'b1;
        cursor_visible [1] = 1'b1;
        app_cursor    [1] = 1'b0;
        char_set      [1] = 1'b0;

        // Insert mode state
        insert_src_col    [8] = 8'd0;
        insert_saved_char [16] = 16'h0000;
        insert_saved_attr [16] = 16'h0000;
        insert_wait       [2] = 2'd0;
        do_tb_rd_en       [1] = 1'b0;

        // UTF-8 decoder
        utf8_accum  [16] = 16'h0000;
        utf8_remain [2]  = 2'd0;

        // Wide character support
        wide_char   [16] = 16'h0000;

        // Kanji binary search state
        kanji_target [16] = 16'h0000;  // Unicode codepoint being searched
        search_lo    [8]  = 8'd0;
        search_hi    [8]  = 8'd255;
        search_found [1]  = 1'b0;      // 1 if match found
        search_slot  [8]  = 8'd0;      // matched slot index

        // TEST1 draw state
        test_row         [6] = 6'd0;
        test_col         [8] = 8'd0;
        test_after_clear [1] = 1'b0;

        // Data acknowledge pulse
        do_data_ack [1] = 1'b0;

    }

    MEM(TYPE=DISTRIBUTED) {
        cmd_buf [8] [80] = 8'h00 { OUT rd SYNC; IN wr; };
    }

    MEM(TYPE=BLOCK) {
        // Kanji lookup ROM: 256 entries of 16-bit Unicode codepoints (sorted)
        // Binary search maps CJK U+4E00-9FFF to slot index 0-255
        kanji_lut [16] [256] = @file("../out/kanji_lut.mem") { OUT rd SYNC; };
    }

    ASYNCHRONOUS {
        scroll   <= do_scroll & ~use_alt_r;
        wr_en    <= do_write;
        wr_row   <= write_row;
        wr_col   <= write_col;
        wr_char  <= write_char;

        // Kanji binary search: midpoint = (lo + hi) / 2
        search_mid_sum <= { 1'b0, search_lo } + { 1'b0, search_hi };
        search_mid     <= search_mid_sum[8:1];

        // Construct attribute word with inverse/conceal applied
        IF (attr_inverse == 1'b1) {
            actual_fg <= bg_idx;
            actual_bg <= fg_idx;
        } ELSE {
            actual_fg <= fg_idx;
            actual_bg <= bg_idx;
        }
        IF (attr_conceal == 1'b1) {
            wr_attr_word <= { actual_bg, actual_bg, attr_underline, attr_slow_blink, attr_fast_blink, attr_strikethrough, 4'b0000 };
        } ELSE {
            wr_attr_word <= { actual_fg, actual_bg, attr_underline, attr_slow_blink, attr_fast_blink, attr_strikethrough, 4'b0000 };
        }
        IF (use_write_attr == 1'b1) {
            wr_attr <= write_attr;
        } ELSE {
            wr_attr <= wr_attr_word;
        }

        cmd_ready   <= cmd_ready_r;
        cmd_final   <= cmd_final_r;
        cmd_len     <= cmd_len_r;
        cmd_rd_data <= cmd_rd_out;

        scroll_reset <= do_scroll_reset & ~use_alt_r;
        scroll_down  <= do_scroll_down & ~use_alt_r;
        tx_data      <= tx_byte_r;
        tx_valid     <= do_tx;

        edid_field    <= edid_field_r;
        edid_byte_idx <= edid_byte_idx_r;
        data_ack      <= do_data_ack;
        use_alt       <= use_alt_r;

        // Data byte index within EDID response (resp_idx - 8)
        edid_data_idx <= resp_idx - 5'd8;

        // param_val * 10 = param_val * 8 + param_val * 2
        param_x10 <= { param_val[4:0], 3'b000 } + { param_val[6:0], 1'b0 };

        // Terminal buffer read-back
        tb_rd_row <= write_row;
        tb_rd_col <= insert_src_col;
        tb_rd_en  <= do_tb_rd_en;

        // DCS match: expected bytes for "EDID" or "TEST1"
        SELECT (dcs_match_idx) {
            CASE (4'd0) { dcs_expected <= 8'h45; }
            CASE (4'd1) { dcs_expected <= 8'h44; }
            CASE (4'd2) { dcs_expected <= 8'h49; }
            CASE (4'd3) { dcs_expected <= 8'h44; }
            DEFAULT     { dcs_expected <= 8'h00; }
        }
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        // Command buffer read port (1-cycle latency)
        cmd_buf.rd.addr <= cmd_rd_addr;
        cmd_rd_out <= cmd_buf.rd.data;

        // ESC_EMIT: write saved character (runs without valid gate)
        IF (state == lit(5, ST_ESC_EMIT)) {
            write_char <= { 8'h00, saved_char };
            write_row  <= cursor_row;
            write_col  <= cursor_col;
            do_write   <= 1'b1;
            use_write_attr <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_scroll_down  <= 1'b0;
            do_tx           <= 1'b0;
            do_tb_rd_en     <= 1'b0;
            do_data_ack     <= 1'b0;
            state      <= lit(5, ST_NORMAL);

            IF (cursor_col == lit(8, MAX_COL)) {
                IF (autowrap == 1'b1) {
                    cursor_col <= 8'd0;
                    IF (cursor_row == lit(6, MAX_ROW)) {
                        do_scroll <= 1'b1;
                    } ELSE {
                        cursor_row <= cursor_row + 6'd1;
                        do_scroll <= 1'b0;
                    }
                } ELSE {
                    do_scroll <= 1'b0;
                }
            } ELSE {
                cursor_col <= cursor_col + 8'd1;
                do_scroll <= 1'b0;
            }

        } ELIF (state == lit(5, ST_CSI_COMMIT)) {
            do_write       <= 1'b0;
            do_scroll      <= 1'b0;
            do_scroll_down <= 1'b0;
            do_tx          <= 1'b0;
            do_tb_rd_en    <= 1'b0;
            cmd_ready_r    <= 1'b0;
            do_data_ack    <= 1'b0;

            SELECT (cmd_final_r) {
                CASE (lit(8, CHAR_m)) {
                    // SGR: commit pending colors and attributes
                    fg_idx             <= pend_fg_idx;
                    bg_idx             <= pend_bg_idx;
                    attr_underline     <= pend_underline;
                    attr_slow_blink    <= pend_slow_blink;
                    attr_fast_blink    <= pend_fast_blink;
                    attr_inverse       <= pend_inverse;
                    attr_conceal       <= pend_conceal;
                    attr_strikethrough <= pend_strikethrough;
                    do_scroll_reset    <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                }
                CASE (lit(8, CHAR_J)) {
                    // Erase in Display
                    SELECT (param_val) {
                        CASE (8'd0) {
                            clr_row <= cursor_row;
                            clr_col <= cursor_col;
                            clr_end_row <= lit(6, MAX_ROW);
                            clr_end_col <= lit(8, MAX_COL);
                            do_scroll_reset <= 1'b0;
                            state <= lit(5, ST_CLEAR);
                        }
                        CASE (8'd1) {
                            clr_row <= 6'd0;
                            clr_col <= 8'd0;
                            clr_end_row <= cursor_row;
                            clr_end_col <= cursor_col;
                            do_scroll_reset <= 1'b0;
                            state <= lit(5, ST_CLEAR);
                        }
                        CASE (8'd2) {
                            clr_row <= 6'd0;
                            clr_col <= 8'd0;
                            clr_end_row <= lit(6, MAX_ROW);
                            clr_end_col <= lit(8, MAX_COL);
                            do_scroll_reset <= 1'b0;
                            state <= lit(5, ST_CLEAR);
                        }
                        CASE (8'd3) {
                            clr_row <= 6'd0;
                            clr_col <= 8'd0;
                            clr_end_row <= lit(6, MAX_ROW);
                            clr_end_col <= lit(8, MAX_COL);
                            do_scroll_reset <= 1'b1;
                            state <= lit(5, ST_CLEAR);
                        }
                        DEFAULT {
                            do_scroll_reset <= 1'b0;
                            state <= lit(5, ST_NORMAL);
                        }
                    }
                }
                CASE (lit(8, CHAR_H)) {
                    // Cursor Position (1-based, default 1)
                    do_scroll_reset <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                    IF (has_semi == 1'b1) {
                        IF (param1 == 8'd0) {
                            cursor_row <= 6'd0;
                        } ELIF (param1 > 8'd45) {
                            cursor_row <= lit(6, MAX_ROW);
                        } ELSE {
                            cursor_row <= param1[5:0] - 6'd1;
                        }
                        IF (param_val == 8'd0) {
                            cursor_col <= 8'd0;
                        } ELIF (param_val[7] == 1'b1) {
                            cursor_col <= lit(8, MAX_COL);
                        } ELSE {
                            cursor_col <= param_val - 8'd1;
                        }
                    } ELSE {
                        IF (param_val == 8'd0) {
                            cursor_row <= 6'd0;
                        } ELIF (param_val > 8'd45) {
                            cursor_row <= lit(6, MAX_ROW);
                        } ELSE {
                            cursor_row <= param_val[5:0] - 6'd1;
                        }
                        cursor_col <= 8'd0;
                    }
                }
                CASE (lit(8, CHAR_A)) {
                    // Cursor Up
                    do_scroll_reset <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                    IF (param_val == 8'd0) {
                        IF (cursor_row != 6'd0) {
                            cursor_row <= cursor_row - 6'd1;
                        }
                    } ELIF ({ 2'b00, cursor_row } < param_val) {
                        cursor_row <= 6'd0;
                    } ELSE {
                        cursor_row <= cursor_row - param_val[5:0];
                    }
                }
                CASE (lit(8, CHAR_B_upper)) {
                    // Cursor Down
                    do_scroll_reset <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                    IF (param_val == 8'd0) {
                        IF (cursor_row != lit(6, MAX_ROW)) {
                            cursor_row <= cursor_row + 6'd1;
                        }
                    } ELIF ({ 2'b00, lit(6, MAX_ROW) - cursor_row } < param_val) {
                        cursor_row <= lit(6, MAX_ROW);
                    } ELSE {
                        cursor_row <= cursor_row + param_val[5:0];
                    }
                }
                CASE (lit(8, CHAR_C)) {
                    // Cursor Forward
                    do_scroll_reset <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                    IF (param_val == 8'd0) {
                        IF (cursor_col != lit(8, MAX_COL)) {
                            cursor_col <= cursor_col + 8'd1;
                        }
                    } ELIF ({ 1'b0, lit(8, MAX_COL) - cursor_col } < { 1'b0, param_val }) {
                        cursor_col <= lit(8, MAX_COL);
                    } ELSE {
                        cursor_col <= cursor_col + param_val;
                    }
                }
                CASE (lit(8, CHAR_D)) {
                    // Cursor Back
                    do_scroll_reset <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                    IF (param_val == 8'd0) {
                        IF (cursor_col != 8'd0) {
                            cursor_col <= cursor_col - 8'd1;
                        }
                    } ELIF ({ 1'b0, cursor_col } < { 1'b0, param_val }) {
                        cursor_col <= 8'd0;
                    } ELSE {
                        cursor_col <= cursor_col - param_val;
                    }
                }
                CASE (lit(8, CHAR_E)) {
                    // Cursor Next Line (col=0, row+n)
                    do_scroll_reset <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                    cursor_col <= 8'd0;
                    IF (param_val == 8'd0) {
                        IF (cursor_row != lit(6, MAX_ROW)) {
                            cursor_row <= cursor_row + 6'd1;
                        }
                    } ELIF ({ 2'b00, lit(6, MAX_ROW) - cursor_row } < param_val) {
                        cursor_row <= lit(6, MAX_ROW);
                    } ELSE {
                        cursor_row <= cursor_row + param_val[5:0];
                    }
                }
                CASE (lit(8, CHAR_F)) {
                    // Cursor Previous Line (col=0, row-n)
                    do_scroll_reset <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                    cursor_col <= 8'd0;
                    IF (param_val == 8'd0) {
                        IF (cursor_row != 6'd0) {
                            cursor_row <= cursor_row - 6'd1;
                        }
                    } ELIF ({ 2'b00, cursor_row } < param_val) {
                        cursor_row <= 6'd0;
                    } ELSE {
                        cursor_row <= cursor_row - param_val[5:0];
                    }
                }
                CASE (lit(8, CHAR_G)) {
                    // Cursor to Column (1-based, default 1)
                    do_scroll_reset <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                    IF (param_val == 8'd0) {
                        cursor_col <= 8'd0;
                    } ELIF (param_val[7] == 1'b1) {
                        cursor_col <= lit(8, MAX_COL);
                    } ELSE {
                        cursor_col <= param_val - 8'd1;
                    }
                }
                CASE (lit(8, CHAR_K)) {
                    // Erase in Line
                    do_scroll_reset <= 1'b0;
                    SELECT (param_val) {
                        CASE (8'd0) {
                            clr_row <= cursor_row;
                            clr_col <= cursor_col;
                            clr_end_row <= cursor_row;
                            clr_end_col <= lit(8, MAX_COL);
                            state <= lit(5, ST_CLEAR);
                        }
                        CASE (8'd1) {
                            clr_row <= cursor_row;
                            clr_col <= 8'd0;
                            clr_end_row <= cursor_row;
                            clr_end_col <= cursor_col;
                            state <= lit(5, ST_CLEAR);
                        }
                        CASE (8'd2) {
                            clr_row <= cursor_row;
                            clr_col <= 8'd0;
                            clr_end_row <= cursor_row;
                            clr_end_col <= lit(8, MAX_COL);
                            state <= lit(5, ST_CLEAR);
                        }
                        DEFAULT {
                            state <= lit(5, ST_NORMAL);
                        }
                    }
                }
                CASE (lit(8, CHAR_S)) {
                    // Scroll Up
                    do_scroll_reset <= 1'b0;
                    IF (param_val == 8'd0) {
                        scroll_count <= 8'd1;
                    } ELSE {
                        scroll_count <= param_val;
                    }
                    scroll_dir <= 1'b0;
                    state <= lit(5, ST_SCROLL_MULTI);
                }
                CASE (lit(8, CHAR_T)) {
                    // Scroll Down
                    do_scroll_reset <= 1'b0;
                    IF (param_val == 8'd0) {
                        scroll_count <= 8'd1;
                    } ELSE {
                        scroll_count <= param_val;
                    }
                    scroll_dir <= 1'b1;
                    state <= lit(5, ST_SCROLL_MULTI);
                }
                CASE (lit(8, CHAR_s_lower)) {
                    // Save Cursor Position (+ colors + attributes)
                    do_scroll_reset     <= 1'b0;
                    saved_cursor_row    <= cursor_row;
                    saved_cursor_col    <= cursor_col;
                    saved_fg_idx        <= fg_idx;
                    saved_bg_idx        <= bg_idx;
                    saved_underline     <= attr_underline;
                    saved_slow_blink    <= attr_slow_blink;
                    saved_fast_blink    <= attr_fast_blink;
                    saved_inverse       <= attr_inverse;
                    saved_conceal       <= attr_conceal;
                    saved_strikethrough <= attr_strikethrough;
                    state <= lit(5, ST_NORMAL);
                }
                CASE (lit(8, CHAR_u)) {
                    // Restore Cursor Position (+ colors + attributes)
                    do_scroll_reset    <= 1'b0;
                    cursor_row         <= saved_cursor_row;
                    cursor_col         <= saved_cursor_col;
                    fg_idx             <= saved_fg_idx;
                    bg_idx             <= saved_bg_idx;
                    attr_underline     <= saved_underline;
                    attr_slow_blink    <= saved_slow_blink;
                    attr_fast_blink    <= saved_fast_blink;
                    attr_inverse       <= saved_inverse;
                    attr_conceal       <= saved_conceal;
                    attr_strikethrough <= saved_strikethrough;
                    state <= lit(5, ST_NORMAL);
                }
                CASE (lit(8, CHAR_n)) {
                    // Device Status Report
                    do_scroll_reset <= 1'b0;
                    IF (param_val == 8'd6) {
                        cpr_row   <= { 1'b0, cursor_row } + 7'd1;
                        cpr_col   <= cursor_col + 8'd1;
                        resp_type <= 2'd1;
                        resp_idx  <= 5'd0;
                        resp_len  <= 5'd9;
                        state <= lit(5, ST_RESP_COMPUTE);
                    } ELSE {
                        state <= lit(5, ST_NORMAL);
                    }
                }
                CASE (lit(8, CHAR_q)) {
                    // Terminal version report (CSI > q)
                    do_scroll_reset <= 1'b0;
                    IF (csi_gt == 1'b1) {
                        resp_type <= 2'd2;
                        resp_idx  <= 5'd0;
                        resp_len  <= 5'd25;
                        state <= lit(5, ST_RESP_SEND);
                    } ELSE {
                        state <= lit(5, ST_NORMAL);
                    }
                }
                CASE (lit(8, CHAR_h)) {
                    // Set Mode
                    do_scroll_reset <= 1'b0;
                    IF (csi_question == 1'b1) {
                        // Private modes (CSI ? ... h)
                        IF (param_val == 8'd25 && param_overflow == 1'b1) {
                            // ?1049h — switch to alt screen
                            alt_cursor_row <= cursor_row;
                            alt_cursor_col <= cursor_col;
                            cursor_row     <= 6'd0;
                            cursor_col     <= 8'd0;
                            use_alt_r      <= 1'b1;
                        } ELSE {
                            SELECT (param_val) {
                                CASE (8'd1)  { app_cursor     <= 1'b1; }
                                CASE (8'd7)  { autowrap       <= 1'b1; }
                                CASE (8'd25) { cursor_visible <= 1'b1; }
                                DEFAULT { }
                            }
                        }
                    } ELSE {
                        // Standard modes (CSI ... h)
                        SELECT (param_val) {
                            CASE (8'd4)  { insert_mode   <= 1'b1; }
                            CASE (8'd20) { newline_mode  <= 1'b1; }
                            DEFAULT { }
                        }
                    }
                    state <= lit(5, ST_NORMAL);
                }
                CASE (lit(8, CHAR_l)) {
                    // Reset Mode
                    do_scroll_reset <= 1'b0;
                    IF (csi_question == 1'b1) {
                        IF (param_val == 8'd25 && param_overflow == 1'b1) {
                            // ?1049l — switch back to main screen
                            cursor_row <= alt_cursor_row;
                            cursor_col <= alt_cursor_col;
                            use_alt_r  <= 1'b0;
                        } ELSE {
                            SELECT (param_val) {
                                CASE (8'd1)  { app_cursor     <= 1'b0; }
                                CASE (8'd7)  { autowrap       <= 1'b0; }
                                CASE (8'd25) { cursor_visible <= 1'b0; }
                                DEFAULT { }
                            }
                        }
                    } ELSE {
                        SELECT (param_val) {
                            CASE (8'd4)  { insert_mode   <= 1'b0; }
                            CASE (8'd20) { newline_mode  <= 1'b0; }
                            DEFAULT { }
                        }
                    }
                    state <= lit(5, ST_NORMAL);
                }
                DEFAULT {
                    do_scroll_reset <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                }
            }

        } ELIF (state == lit(5, ST_CLEAR)) {
            // Screen clear: write one blank cell per cycle
            do_scroll      <= 1'b0;
            do_scroll_down <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx          <= 1'b0;
            do_tb_rd_en    <= 1'b0;
            cmd_ready_r    <= 1'b0;
            do_data_ack    <= 1'b0;
            IF (busy == 1'b0) {
                write_char     <= 16'h0000;
                write_row      <= clr_row;
                write_col      <= clr_col;
                do_write       <= 1'b1;
                use_write_attr <= 1'b0;
                IF (clr_row == clr_end_row && clr_col == clr_end_col) {
                    IF (test_after_clear == 1'b1) {
                        test_after_clear <= 1'b0;
                        test_row <= 6'd0;
                        test_col <= 8'd0;
                        state <= lit(5, ST_TEST_DRAW);
                    } ELSE {
                        state <= lit(5, ST_NORMAL);
                    }
                } ELIF (clr_col == lit(8, MAX_COL)) {
                    clr_col <= 8'd0;
                    clr_row <= clr_row + 6'd1;
                } ELSE {
                    clr_col <= clr_col + 8'd1;
                }
            } ELSE {
                do_write <= 1'b0;
            }

        } ELIF (state == lit(5, ST_TEST_DRAW)) {
            // Draw ruler test pattern (one cell per SDRAM write cycle)
            do_scroll      <= 1'b0;
            do_scroll_down <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx          <= 1'b0;
            do_tb_rd_en    <= 1'b0;
            cmd_ready_r    <= 1'b0;
            do_data_ack    <= 1'b0;

            IF (busy == 1'b0) {
                write_row      <= test_row;
                write_col      <= test_col;
                do_write       <= 1'b1;
                use_write_attr <= 1'b0;

                // Determine character for this cell
                IF (test_row == 6'd0 && test_col == 8'd0) {
                    // Origin: '+'
                    write_char <= 16'h002B;
                } ELIF (test_row == 6'd0) {
                    // Top ruler: cycling "1234567890" (col mod 10)
                    SELECT (test_col - 8'd1) {
                        CASE (8'd0)  { write_char <= 16'h0031; }
                        CASE (8'd10) { write_char <= 16'h0031; }
                        CASE (8'd20) { write_char <= 16'h0031; }
                        CASE (8'd30) { write_char <= 16'h0031; }
                        CASE (8'd40) { write_char <= 16'h0031; }
                        CASE (8'd50) { write_char <= 16'h0031; }
                        CASE (8'd60) { write_char <= 16'h0031; }
                        CASE (8'd70) { write_char <= 16'h0031; }
                        CASE (8'd80) { write_char <= 16'h0031; }
                        CASE (8'd90) { write_char <= 16'h0031; }
                        CASE (8'd100) { write_char <= 16'h0031; }
                        CASE (8'd110) { write_char <= 16'h0031; }
                        CASE (8'd120) { write_char <= 16'h0031; }
                        CASE (8'd130) { write_char <= 16'h0031; }
                        CASE (8'd140) { write_char <= 16'h0031; }
                        CASE (8'd9)  { write_char <= 16'h007C; }
                        CASE (8'd19) { write_char <= 16'h007C; }
                        CASE (8'd29) { write_char <= 16'h007C; }
                        CASE (8'd39) { write_char <= 16'h007C; }
                        CASE (8'd49) { write_char <= 16'h007C; }
                        CASE (8'd59) { write_char <= 16'h007C; }
                        CASE (8'd69) { write_char <= 16'h007C; }
                        CASE (8'd79) { write_char <= 16'h007C; }
                        CASE (8'd89) { write_char <= 16'h007C; }
                        CASE (8'd99) { write_char <= 16'h007C; }
                        CASE (8'd109) { write_char <= 16'h007C; }
                        CASE (8'd119) { write_char <= 16'h007C; }
                        CASE (8'd129) { write_char <= 16'h007C; }
                        CASE (8'd139) { write_char <= 16'h007C; }
                        DEFAULT {
                            // Use position mod 10 to pick digit
                            // Simplified: just write dots for non-decade positions
                            write_char <= 16'h002E;
                        }
                    }
                } ELIF (test_col == 8'd0) {
                    // Left ruler: cycling "1234567890" (row mod 10)
                    SELECT ({ 2'b00, test_row } - 8'd1) {
                        CASE (8'd0)  { write_char <= 16'h0031; }
                        CASE (8'd10) { write_char <= 16'h0031; }
                        CASE (8'd20) { write_char <= 16'h0031; }
                        CASE (8'd30) { write_char <= 16'h0031; }
                        CASE (8'd40) { write_char <= 16'h0031; }
                        CASE (8'd9)  { write_char <= 16'h002D; }
                        CASE (8'd19) { write_char <= 16'h002D; }
                        CASE (8'd29) { write_char <= 16'h002D; }
                        CASE (8'd39) { write_char <= 16'h002D; }
                        DEFAULT      { write_char <= 16'h002E; }
                    }
                } ELSE {
                    write_char <= 16'h0020;
                }

                // Advance position
                IF (test_col == lit(8, MAX_COL)) {
                    test_col <= 8'd0;
                    IF (test_row == lit(6, MAX_ROW)) {
                        // Done — cursor to 0,0
                        cursor_row <= 6'd0;
                        cursor_col <= 8'd0;
                        state <= lit(5, ST_NORMAL);
                    } ELSE {
                        test_row <= test_row + 6'd1;
                    }
                } ELSE {
                    test_col <= test_col + 8'd1;
                }
            } ELSE {
                do_write <= 1'b0;
            }

        } ELIF (state == lit(5, ST_SCROLL_MULTI)) {
            // Multi-line scroll loop
            do_write       <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx          <= 1'b0;
            do_tb_rd_en    <= 1'b0;
            cmd_ready_r    <= 1'b0;
            do_data_ack    <= 1'b0;
            IF (scroll_count == 8'd0) {
                do_scroll      <= 1'b0;
                do_scroll_down <= 1'b0;
                state <= lit(5, ST_NORMAL);
            } ELIF (busy == 1'b0) {
                scroll_count <= scroll_count - 8'd1;
                IF (scroll_dir == 1'b0) {
                    do_scroll      <= 1'b1;
                    do_scroll_down <= 1'b0;
                } ELSE {
                    do_scroll      <= 1'b0;
                    do_scroll_down <= 1'b1;
                }
            } ELSE {
                do_scroll      <= 1'b0;
                do_scroll_down <= 1'b0;
            }

        } ELIF (state == lit(5, ST_INSERT_READ)) {
            // Insert mode: read cell at insert_src_col, shift right
            do_scroll      <= 1'b0;
            do_scroll_down <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx          <= 1'b0;
            cmd_ready_r    <= 1'b0;
            do_data_ack    <= 1'b0;

            IF (insert_wait == 2'd0) {
                // Start read from buffer
                do_write       <= 1'b0;
                use_write_attr <= 1'b0;
                do_tb_rd_en    <= 1'b1;
                insert_wait    <= 2'd1;
            } ELIF (insert_wait == 2'd1) {
                // Wait for BSRAM latency
                do_write       <= 1'b0;
                use_write_attr <= 1'b0;
                do_tb_rd_en    <= 1'b0;
                insert_wait    <= 2'd2;
            } ELIF (insert_wait == 2'd2) {
                // Data ready — write it to insert_src_col+1
                do_tb_rd_en    <= 1'b0;
                insert_wait    <= 2'd0;
                write_row      <= cursor_row;
                write_col      <= insert_src_col + 8'd1;
                write_char     <= tb_rd_char;
                write_attr     <= tb_rd_attr;
                use_write_attr <= 1'b1;
                do_write       <= 1'b1;

                IF (insert_src_col == cursor_col) {
                    // Done shifting — write saved char at cursor position
                    state <= lit(5, ST_INSERT_WRITE);
                } ELSE {
                    insert_src_col <= insert_src_col - 8'd1;
                }
            } ELSE {
                do_write       <= 1'b0;
                use_write_attr <= 1'b0;
                do_tb_rd_en    <= 1'b0;
                insert_wait    <= 2'd0;
            }

        } ELIF (state == lit(5, ST_INSERT_WRITE)) {
            // Insert mode: write saved char at cursor position and advance
            do_scroll_down <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx          <= 1'b0;
            do_tb_rd_en    <= 1'b0;
            cmd_ready_r    <= 1'b0;
            do_data_ack    <= 1'b0;

            write_char     <= insert_saved_char;
            write_attr     <= insert_saved_attr;
            use_write_attr <= 1'b1;
            write_row      <= cursor_row;
            write_col      <= cursor_col;
            do_write       <= 1'b1;
            state      <= lit(5, ST_NORMAL);

            // Advance cursor
            IF (cursor_col == lit(8, MAX_COL)) {
                IF (autowrap == 1'b1) {
                    cursor_col <= 8'd0;
                    IF (cursor_row == lit(6, MAX_ROW)) {
                        do_scroll <= 1'b1;
                    } ELSE {
                        do_scroll <= 1'b0;
                        cursor_row <= cursor_row + 6'd1;
                    }
                } ELSE {
                    do_scroll <= 1'b0;
                }
            } ELSE {
                do_scroll <= 1'b0;
                cursor_col <= cursor_col + 8'd1;
            }

        } ELIF (state == lit(5, ST_RESP_COMPUTE)) {
            // Compute BCD digits for CPR response (one cycle)
            do_write       <= 1'b0;
            do_scroll      <= 1'b0;
            do_scroll_down <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx          <= 1'b0;
            do_tb_rd_en    <= 1'b0;
            cmd_ready_r    <= 1'b0;
            do_data_ack    <= 1'b0;

            // Row BCD (1-45)
            IF (cpr_row >= 7'd40) {
                cpr_row_tens <= 4'd4;
                cpr_row_ones <= cpr_row - 7'd40;
            } ELIF (cpr_row >= 7'd30) {
                cpr_row_tens <= 4'd3;
                cpr_row_ones <= cpr_row - 7'd30;
            } ELIF (cpr_row >= 7'd20) {
                cpr_row_tens <= 4'd2;
                cpr_row_ones <= cpr_row - 7'd20;
            } ELIF (cpr_row >= 7'd10) {
                cpr_row_tens <= 4'd1;
                cpr_row_ones <= cpr_row - 7'd10;
            } ELSE {
                cpr_row_tens <= 4'd0;
                cpr_row_ones <= cpr_row;
            }

            // Col BCD (1-142)
            IF (cpr_col >= 8'd100) {
                cpr_col_hund <= 4'd1;
                IF (cpr_col >= 8'd140) {
                    cpr_col_tens <= 8'd4;
                    cpr_col_ones <= cpr_col - 8'd140;
                } ELIF (cpr_col >= 8'd130) {
                    cpr_col_tens <= 8'd3;
                    cpr_col_ones <= cpr_col - 8'd130;
                } ELIF (cpr_col >= 8'd120) {
                    cpr_col_tens <= 8'd2;
                    cpr_col_ones <= cpr_col - 8'd120;
                } ELIF (cpr_col >= 8'd110) {
                    cpr_col_tens <= 8'd1;
                    cpr_col_ones <= cpr_col - 8'd110;
                } ELSE {
                    cpr_col_tens <= 8'd0;
                    cpr_col_ones <= cpr_col - 8'd100;
                }
            } ELSE {
                cpr_col_hund <= 4'd0;
                IF (cpr_col >= 8'd90) {
                    cpr_col_tens <= 8'd9;
                    cpr_col_ones <= cpr_col - 8'd90;
                } ELIF (cpr_col >= 8'd80) {
                    cpr_col_tens <= 8'd8;
                    cpr_col_ones <= cpr_col - 8'd80;
                } ELIF (cpr_col >= 8'd70) {
                    cpr_col_tens <= 8'd7;
                    cpr_col_ones <= cpr_col - 8'd70;
                } ELIF (cpr_col >= 8'd60) {
                    cpr_col_tens <= 8'd6;
                    cpr_col_ones <= cpr_col - 8'd60;
                } ELIF (cpr_col >= 8'd50) {
                    cpr_col_tens <= 8'd5;
                    cpr_col_ones <= cpr_col - 8'd50;
                } ELIF (cpr_col >= 8'd40) {
                    cpr_col_tens <= 8'd4;
                    cpr_col_ones <= cpr_col - 8'd40;
                } ELIF (cpr_col >= 8'd30) {
                    cpr_col_tens <= 8'd3;
                    cpr_col_ones <= cpr_col - 8'd30;
                } ELIF (cpr_col >= 8'd20) {
                    cpr_col_tens <= 8'd2;
                    cpr_col_ones <= cpr_col - 8'd20;
                } ELIF (cpr_col >= 8'd10) {
                    cpr_col_tens <= 8'd1;
                    cpr_col_ones <= cpr_col - 8'd10;
                } ELSE {
                    cpr_col_tens <= 8'd0;
                    cpr_col_ones <= cpr_col;
                }
            }

            state <= lit(5, ST_RESP_SEND);

        } ELIF (state == lit(5, ST_RESP_SEND)) {
            // Send response bytes via UART TX
            do_write       <= 1'b0;
            do_scroll      <= 1'b0;
            do_scroll_down <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tb_rd_en    <= 1'b0;
            cmd_ready_r    <= 1'b0;
            do_data_ack    <= 1'b0;

            IF (resp_idx == resp_len) {
                do_tx <= 1'b0;
                IF (resp_type == 2'd3 && edid_phase != 2'd3) {
                    edid_phase <= edid_phase + 2'd1;
                    resp_idx   <= 5'd0;
                    SELECT (edid_phase) {
                        CASE (2'd0) { resp_len <= 5'd23; }
                        CASE (2'd1) { resp_len <= 5'd23; }
                        CASE (2'd2) { resp_len <= 5'd19; }
                        DEFAULT     { resp_len <= 5'd0; }
                    }
                } ELSE {
                    state <= lit(5, ST_NORMAL);
                }
            } ELIF (tx_ready == 1'b1 && do_tx == 1'b0) {

                IF (resp_type == 2'd3 && resp_idx > 5'd7 && resp_idx + 5'd2 < resp_len) {
                    edid_field_r    <= edid_phase;
                    edid_byte_idx_r <= edid_data_idx[3:0];
                    edid_wait       <= 2'd0;
                    do_tx           <= 1'b0;
                    state           <= lit(5, ST_EDID_FETCH);
                } ELSE {
                    do_tx <= 1'b1;
                    resp_idx <= resp_idx + 5'd1;

                    IF (resp_type == 2'd1) {
                        SELECT (resp_idx) {
                            CASE (5'd0) { tx_byte_r <= 8'h1B; }
                            CASE (5'd1) { tx_byte_r <= 8'h5B; }
                            CASE (5'd2) { tx_byte_r <= { 4'b0011, cpr_row_tens }; }
                            CASE (5'd3) { tx_byte_r <= { 1'b0, cpr_row_ones } + 8'h30; }
                            CASE (5'd4) { tx_byte_r <= 8'h3B; }
                            CASE (5'd5) { tx_byte_r <= { 4'b0011, cpr_col_hund }; }
                            CASE (5'd6) { tx_byte_r <= cpr_col_tens + 8'h30; }
                            CASE (5'd7) { tx_byte_r <= cpr_col_ones + 8'h30; }
                            CASE (5'd8) { tx_byte_r <= lit(8, CHAR_R); }
                            DEFAULT     { tx_byte_r <= 8'h00; }
                        }
                    } ELIF (resp_type == 2'd2) {
                        SELECT (resp_idx) {
                            CASE (5'd0)  { tx_byte_r <= 8'h1B; }
                            CASE (5'd1)  { tx_byte_r <= 8'h50; }
                            CASE (5'd2)  { tx_byte_r <= 8'h3E; }
                            CASE (5'd3)  { tx_byte_r <= 8'h7C; }
                            CASE (5'd4)  { tx_byte_r <= 8'h53; }
                            CASE (5'd5)  { tx_byte_r <= 8'h69; }
                            CASE (5'd6)  { tx_byte_r <= 8'h6D; }
                            CASE (5'd7)  { tx_byte_r <= 8'h70; }
                            CASE (5'd8)  { tx_byte_r <= 8'h6C; }
                            CASE (5'd9)  { tx_byte_r <= 8'h65; }
                            CASE (5'd10) { tx_byte_r <= 8'h20; }
                            CASE (5'd11) { tx_byte_r <= 8'h54; }
                            CASE (5'd12) { tx_byte_r <= 8'h65; }
                            CASE (5'd13) { tx_byte_r <= 8'h72; }
                            CASE (5'd14) { tx_byte_r <= 8'h6D; }
                            CASE (5'd15) { tx_byte_r <= 8'h69; }
                            CASE (5'd16) { tx_byte_r <= 8'h6E; }
                            CASE (5'd17) { tx_byte_r <= 8'h61; }
                            CASE (5'd18) { tx_byte_r <= 8'h6C; }
                            CASE (5'd19) { tx_byte_r <= 8'h20; }
                            CASE (5'd20) { tx_byte_r <= 8'h31; }
                            CASE (5'd21) { tx_byte_r <= 8'h2E; }
                            CASE (5'd22) { tx_byte_r <= 8'h30; }
                            CASE (5'd23) { tx_byte_r <= 8'h1B; }
                            CASE (5'd24) { tx_byte_r <= 8'h5C; }
                            DEFAULT      { tx_byte_r <= 8'h00; }
                        }
                    } ELIF (resp_type == 2'd3) {
                        // EDID response: ESC P <n> EDID=<data> ESC \
                        SELECT (resp_idx) {
                            CASE (5'd0)  { tx_byte_r <= 8'h1B; }
                            CASE (5'd1)  { tx_byte_r <= 8'h50; }
                            CASE (5'd2)  {
                                SELECT (edid_phase) {
                                    CASE (2'd0) { tx_byte_r <= 8'h31; }
                                    CASE (2'd1) { tx_byte_r <= 8'h32; }
                                    CASE (2'd2) { tx_byte_r <= 8'h33; }
                                    CASE (2'd3) { tx_byte_r <= 8'h34; }
                                    DEFAULT     { tx_byte_r <= 8'h31; }
                                }
                            }
                            CASE (5'd3)  { tx_byte_r <= 8'h45; }
                            CASE (5'd4)  { tx_byte_r <= 8'h44; }
                            CASE (5'd5)  { tx_byte_r <= 8'h49; }
                            CASE (5'd6)  { tx_byte_r <= 8'h44; }
                            CASE (5'd7)  { tx_byte_r <= 8'h3D; }
                            DEFAULT {
                                IF (resp_idx + 5'd1 == resp_len) {
                                    tx_byte_r <= 8'h5C;
                                } ELSE {
                                    tx_byte_r <= 8'h1B;
                                }
                            }
                        }
                    } ELSE {
                        tx_byte_r <= 8'h00;
                    }
                }
            } ELSE {
                do_tx <= 1'b0;
            }

        } ELIF (state == lit(5, ST_EDID_FETCH)) {
            // Wait for EDID reader byte (2-cycle MEM read latency)
            do_write       <= 1'b0;
            do_scroll      <= 1'b0;
            do_scroll_down <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tb_rd_en    <= 1'b0;
            cmd_ready_r    <= 1'b0;
            do_data_ack    <= 1'b0;

            IF (edid_wait == 2'd2) {
                IF (edid_byte == 8'h0A || edid_byte == 8'h00) {
                    tx_byte_r <= 8'h20;
                } ELSE {
                    tx_byte_r <= edid_byte;
                }
                do_tx    <= 1'b1;
                resp_idx <= resp_idx + 5'd1;
                state    <= lit(5, ST_RESP_SEND);
            } ELSE {
                do_tx     <= 1'b0;
                edid_wait <= edid_wait + 2'd1;
            }

        } ELIF (state == lit(5, ST_WIDE_RIGHT)) {
            // Write right half of wide character (char | 0x8000)
            // Must wait for terminal_buffer to be ready — if the left-half
            // write was latched into the single pend_wr slot (e.g. during
            // prefetch), busy stays high until that write completes.
            do_scroll_down <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx          <= 1'b0;
            do_tb_rd_en    <= 1'b0;
            do_data_ack    <= 1'b0;

            IF (busy == 1'b0) {
                write_char     <= wide_char | 16'h8000;
                write_row      <= cursor_row;
                write_col      <= cursor_col + 8'd1;
                do_write       <= 1'b1;
                use_write_attr <= 1'b0;

                // Advance cursor by 2 from original position
                IF (cursor_col + 8'd1 >= lit(8, MAX_COL)) {
                    // Wide char ends at or past last column — wrap
                    IF (autowrap == 1'b1) {
                        cursor_col <= 8'd0;
                        IF (cursor_row == lit(6, MAX_ROW)) {
                            do_scroll <= 1'b1;
                        } ELSE {
                            cursor_row <= cursor_row + 6'd1;
                            do_scroll <= 1'b0;
                        }
                    } ELSE {
                        do_scroll <= 1'b0;
                    }
                } ELSE {
                    cursor_col <= cursor_col + 8'd2;
                    do_scroll <= 1'b0;
                }
                state <= lit(5, ST_NORMAL);
            } ELSE {
                // Buffer busy — wait, don't write yet
                do_write <= 1'b0;
                do_scroll <= 1'b0;
            }

        } ELIF (state == lit(5, ST_WIDE_WRAP)) {
            // After wrap/scroll completes, write left half at col 0
            // Must wait for busy=0 (scroll sets clearing=1 which sets busy=1)
            do_scroll_down <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx          <= 1'b0;
            do_tb_rd_en    <= 1'b0;
            do_data_ack    <= 1'b0;

            IF (busy == 1'b0) {
                write_char     <= wide_char;
                write_row      <= cursor_row;
                write_col      <= cursor_col;
                do_write       <= 1'b1;
                use_write_attr <= 1'b0;
                do_scroll      <= 1'b0;
                state          <= lit(5, ST_WIDE_RIGHT);
            } ELSE {
                // Buffer still busy (scroll/clear in progress) — wait
                do_write <= 1'b0;
                do_scroll <= 1'b0;
            }

        // === Kanji binary search state machine ===
        // Uses BSRAM for kanji_lut, so reads have 1-cycle latency.
        // ST_KANJI_READ: drive address (search_mid), wait for BSRAM
        // ST_KANJI_CMP:  compare result, narrow range, loop or done
        } ELIF (state == lit(5, ST_KANJI_READ)) {
            do_scroll_down  <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx           <= 1'b0;
            do_tb_rd_en     <= 1'b0;
            do_data_ack     <= 1'b0;
            do_write        <= 1'b0;
            do_scroll       <= 1'b0;

            // Drive BSRAM read address; data available next cycle
            kanji_lut.rd.addr <= search_mid;
            // Also register search_mid so ST_KANJI_CMP knows which slot matched
            search_slot <= search_mid;
            state       <= lit(5, ST_KANJI_CMP);

        } ELIF (state == lit(5, ST_KANJI_CMP)) {
            do_scroll_down  <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx           <= 1'b0;
            do_tb_rd_en     <= 1'b0;
            do_data_ack     <= 1'b0;
            do_write        <= 1'b0;
            do_scroll       <= 1'b0;

            IF (search_lo > search_hi) {
                // Search exhausted — not found
                state <= lit(5, ST_KANJI_DONE);
            } ELIF (kanji_lut.rd.data == kanji_target) {
                // Found! slot = search_slot (registered from previous cycle)
                search_found <= 1'b1;
                state        <= lit(5, ST_KANJI_DONE);
            } ELIF (kanji_lut.rd.data < kanji_target) {
                search_lo <= search_slot + 8'd1;
                state     <= lit(5, ST_KANJI_READ);
            } ELSE {
                IF (search_slot == 8'd0) {
                    // Prevent underflow — not found
                    state <= lit(5, ST_KANJI_DONE);
                } ELSE {
                    search_hi <= search_slot - 8'd1;
                    state     <= lit(5, ST_KANJI_READ);
                }
            }

        } ELIF (state == lit(5, ST_KANJI_DONE)) {
            // Search complete — if found, write as wide char at U+4000+slot
            do_scroll_down  <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx           <= 1'b0;
            do_tb_rd_en     <= 1'b0;
            do_data_ack     <= 1'b0;

            IF (search_found == 1'b1) {
                // Remap to internal code U+4000 + slot
                wide_char      <= { 8'h40, search_slot };
                use_write_attr <= 1'b0;

                IF (cursor_col >= lit(8, MAX_COL)) {
                    IF (autowrap == 1'b1) {
                        cursor_col <= 8'd0;
                        IF (cursor_row == lit(6, MAX_ROW)) {
                            do_scroll <= 1'b1;
                        } ELSE {
                            cursor_row <= cursor_row + 6'd1;
                            do_scroll  <= 1'b0;
                        }
                        state <= lit(5, ST_WIDE_WRAP);
                    } ELSE {
                        write_char <= { 8'h40, search_slot };
                        write_row  <= cursor_row;
                        write_col  <= cursor_col;
                        do_write   <= 1'b1;
                        do_scroll  <= 1'b0;
                        state      <= lit(5, ST_NORMAL);
                    }
                } ELSE {
                    write_char <= { 8'h40, search_slot };
                    write_row  <= cursor_row;
                    write_col  <= cursor_col;
                    do_write   <= 1'b1;
                    do_scroll  <= 1'b0;
                    state      <= lit(5, ST_WIDE_RIGHT);
                }
            } ELSE {
                // Not found — write fallback character '?' (U+003F)
                write_char     <= 16'h003F;
                write_row      <= cursor_row;
                write_col      <= cursor_col;
                do_write       <= 1'b1;
                use_write_attr <= 1'b0;
                state          <= lit(5, ST_NORMAL);

                IF (cursor_col == lit(8, MAX_COL) && autowrap == 1'b1) {
                    cursor_col <= 8'd0;
                    IF (cursor_row == lit(6, MAX_ROW)) {
                        do_scroll <= 1'b1;
                    } ELSE {
                        cursor_row <= cursor_row + 6'd1;
                        do_scroll <= 1'b0;
                    }
                } ELIF (cursor_col != lit(8, MAX_COL)) {
                    cursor_col <= cursor_col + 8'd1;
                    do_scroll <= 1'b0;
                } ELSE {
                    do_scroll <= 1'b0;
                }
            }

        } ELIF (valid == 1'b1 && busy == 1'b0 && do_data_ack == 1'b0) {
            do_data_ack <= 1'b1;
            SELECT (state) {
                CASE (lit(5, ST_NORMAL)) {
                    IF (data == lit(8, CHAR_ESC)) {
                        // Got ESC
                        state    <= lit(5, ST_ESC);
                        do_write <= 1'b0;
                        do_scroll <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                    } ELIF (data == lit(8, CHAR_CR)) {
                        // CR: move to column 0 and advance row
                        // (serial terminals typically send CR alone on Enter)
                        do_write <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                        cursor_col <= 8'd0;
                        IF (cursor_row == lit(6, MAX_ROW)) {
                            do_scroll <= 1'b1;
                        } ELSE {
                            cursor_row <= cursor_row + 6'd1;
                            do_scroll <= 1'b0;
                        }
                    } ELIF (data == lit(8, CHAR_LF)) {
                        // LF: move down one row; if newline_mode, also col=0
                        do_write <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                        IF (newline_mode == 1'b1) {
                            cursor_col <= 8'd0;
                        }
                        IF (cursor_row == lit(6, MAX_ROW)) {
                            do_scroll <= 1'b1;
                        } ELSE {
                            cursor_row <= cursor_row + 6'd1;
                            do_scroll <= 1'b0;
                        }
                    } ELIF (data == lit(8, CHAR_BEL)) {
                        // BEL: ignored (no audio hardware)
                        do_write <= 1'b0;
                        do_scroll <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx <= 1'b0;
                        do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                    } ELIF (data == lit(8, CHAR_TAB)) {
                        // TAB: advance to next 8-column boundary
                        do_write <= 1'b0;
                        do_scroll <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                        IF ({ cursor_col[7:3], 3'b000 } + 8'd8 > lit(8, MAX_COL)) {
                            cursor_col <= lit(8, MAX_COL);
                        } ELSE {
                            cursor_col <= { cursor_col[7:3] + 5'd1, 3'b000 };
                        }
                    } ELIF (data == lit(8, CHAR_DEL)) {
                        // DEL (backspace): move back one and write space
                        do_scroll <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                        IF (cursor_col != 8'd0) {
                            cursor_col <= cursor_col - 8'd1;
                            write_char     <= 16'h0020;
                            write_row      <= cursor_row;
                            write_col      <= cursor_col - 8'd1;
                            do_write       <= 1'b1;
                            use_write_attr <= 1'b0;
                        } ELSE {
                            do_write <= 1'b0;
                        }
                    } ELIF (data[7:5] == 3'b110) {
                        // UTF-8 2-byte lead (U+0080 - U+07FF)
                        utf8_accum  <= { 11'b00000000000, data[4:0] };
                        utf8_remain <= 2'd1;
                        state       <= lit(5, ST_UTF8);
                        do_write <= 1'b0;
                        do_scroll <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                    } ELIF (data[7:4] == 4'b1110) {
                        // UTF-8 3-byte lead (U+0800 - U+FFFF)
                        utf8_accum  <= { 12'b000000000000, data[3:0] };
                        utf8_remain <= 2'd2;
                        state       <= lit(5, ST_UTF8);
                        do_write <= 1'b0;
                        do_scroll <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                    } ELIF (data[7] == 1'b1) {
                        // Unexpected continuation byte or 4-byte lead — ignore
                        do_write <= 1'b0;
                        do_scroll <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                    } ELSE {
                        // Normal ASCII character — write to terminal buffer
                        IF (insert_mode == 1'b1 && cursor_col != lit(8, MAX_COL)) {
                            // Insert mode: shift right before writing
                            insert_saved_char <= { 8'h00, data };
                            insert_saved_attr <= wr_attr_word;
                            insert_src_col    <= lit(8, MAX_COL) - 8'd1;
                            insert_wait       <= 2'd0;
                            do_write          <= 1'b0;
                            do_scroll         <= 1'b0;
                            do_scroll_down    <= 1'b0;
                            do_scroll_reset   <= 1'b0;
                            do_tx             <= 1'b0;
                            do_tb_rd_en       <= 1'b0;
                            cmd_ready_r       <= 1'b0;
                            write_row         <= cursor_row;
                            state <= lit(5, ST_INSERT_READ);
                        } ELSE {
                            write_char     <= { 8'h00, data };
                            write_row      <= cursor_row;
                            write_col      <= cursor_col;
                            do_write       <= 1'b1;
                            use_write_attr <= 1'b0;
                            do_scroll_down <= 1'b0;
                            do_scroll_reset <= 1'b0;
                            do_tx <= 1'b0;
                                    do_tb_rd_en <= 1'b0;
                            cmd_ready_r <= 1'b0;

                            IF (cursor_col == lit(8, MAX_COL)) {
                                IF (autowrap == 1'b1) {
                                    cursor_col <= 8'd0;
                                    IF (cursor_row == lit(6, MAX_ROW)) {
                                        do_scroll <= 1'b1;
                                    } ELSE {
                                        cursor_row <= cursor_row + 6'd1;
                                        do_scroll <= 1'b0;
                                    }
                                } ELSE {
                                    do_scroll <= 1'b0;
                                }
                            } ELSE {
                                cursor_col <= cursor_col + 8'd1;
                                do_scroll <= 1'b0;
                            }
                        }
                    }
                }

                CASE (lit(5, ST_ESC)) {
                    IF (data == lit(8, CHAR_LBRACKET)) {
                        // ESC [ — enter CSI parameter collection
                        state          <= lit(5, ST_CSI);
                        buf_ptr        <= 7'd0;
                        param_val      <= 8'd0;
                        param1         <= 8'd0;
                        has_semi       <= 1'b0;
                        csi_gt         <= 1'b0;
                        csi_question   <= 1'b0;
                        param_overflow <= 1'b0;
                        // Copy current attributes to pending
                        pend_fg_idx        <= fg_idx;
                        pend_bg_idx        <= bg_idx;
                        pend_underline     <= attr_underline;
                        pend_slow_blink    <= attr_slow_blink;
                        pend_fast_blink    <= attr_fast_blink;
                        pend_inverse       <= attr_inverse;
                        pend_conceal       <= attr_conceal;
                        pend_strikethrough <= attr_strikethrough;
                        do_write   <= 1'b0;
                        do_scroll  <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx      <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                    } ELIF (data == lit(8, CHAR_P)) {
                        // ESC P — enter DCS sequence
                        state          <= lit(5, ST_DCS);
                        dcs_match_idx  <= 4'd0;
                        dcs_matched    <= 1'b1;
                        dcs_test_matched <= 1'b1;
                        do_write       <= 1'b0;
                        do_scroll      <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx          <= 1'b0;
                        do_tb_rd_en    <= 1'b0;
                        cmd_ready_r    <= 1'b0;
                    } ELIF (data == lit(8, CHAR_RBRACKET)) {
                        // ESC ] — enter OSC sequence (consume until BEL or ESC \)
                        state      <= lit(5, ST_OSC);
                        do_write   <= 1'b0;
                        do_scroll  <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx      <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                    } ELIF (data == lit(8, CHAR_LPAREN)) {
                        // ESC ( — character set selection
                        state      <= lit(5, ST_ESC_LPAREN);
                        do_write   <= 1'b0;
                        do_scroll  <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx      <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                    } ELIF (data == lit(8, CHAR_HASH)) {
                        // ESC # — DEC test (consume one more byte)
                        state      <= lit(5, ST_ESC_HASH);
                        do_write   <= 1'b0;
                        do_scroll  <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx      <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                    } ELIF (data == lit(8, CHAR_c_lower) || data == lit(8, CHAR_EQUALS) || data == lit(8, CHAR_GT) || data == lit(8, CHAR_M) || data == lit(8, CHAR_7) || data == lit(8, CHAR_8)) {
                        // ESC c (RIS), ESC = (DECKPAM), ESC > (DECKPNM),
                        // ESC M (RI), ESC 7 (DECSC), ESC 8 (DECRC) — silently ignore
                        state      <= lit(5, ST_NORMAL);
                        do_write   <= 1'b0;
                        do_scroll  <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx      <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        cmd_ready_r <= 1'b0;
                    } ELSE {
                        // Unrecognized ESC sequence — write ESC then saved byte
                        write_char     <= { 8'h00, lit(8, CHAR_ESC) };
                        write_row      <= cursor_row;
                        write_col      <= cursor_col;
                        do_write       <= 1'b1;
                        use_write_attr <= 1'b0;
                        do_scroll_down <= 1'b0;
                        do_scroll_reset <= 1'b0;
                        do_tx      <= 1'b0;
                            do_tb_rd_en <= 1'b0;
                        saved_char <= data;
                        state      <= lit(5, ST_ESC_EMIT);
                        cmd_ready_r <= 1'b0;

                        IF (cursor_col == lit(8, MAX_COL)) {
                            IF (autowrap == 1'b1) {
                                cursor_col <= 8'd0;
                                IF (cursor_row == lit(6, MAX_ROW)) {
                                    do_scroll <= 1'b1;
                                } ELSE {
                                    cursor_row <= cursor_row + 6'd1;
                                    do_scroll <= 1'b0;
                                }
                            } ELSE {
                                do_scroll <= 1'b0;
                            }
                        } ELSE {
                            cursor_col <= cursor_col + 8'd1;
                            do_scroll <= 1'b0;
                        }
                    }
                }

                CASE (lit(5, ST_ESC_LPAREN)) {
                    // Character set: ESC ( B = ASCII, ESC ( U = CP437
                    do_write  <= 1'b0;
                    do_scroll <= 1'b0;
                    do_scroll_down <= 1'b0;
                    do_scroll_reset <= 1'b0;
                    do_tx     <= 1'b0;
                    do_tb_rd_en <= 1'b0;
                    cmd_ready_r <= 1'b0;

                    IF (data == lit(8, CHAR_B_upper)) {
                        char_set <= 1'b0;
                    } ELIF (data == lit(8, CHAR_U_upper)) {
                        char_set <= 1'b1;
                    }
                    state <= lit(5, ST_NORMAL);
                }

                CASE (lit(5, ST_CSI)) {
                    do_write  <= 1'b0;
                    do_scroll <= 1'b0;
                    do_scroll_down <= 1'b0;
                    do_scroll_reset <= 1'b0;
                    do_tx     <= 1'b0;
                    do_tb_rd_en <= 1'b0;

                    IF ((data >= lit(8, CHAR_0) && data <= lit(8, CHAR_9)) || data == lit(8, CHAR_SEMI) || data == lit(8, CHAR_GT) || data == lit(8, CHAR_QUESTION)) {
                        // Digit, semicolon, '>', or '?' — store in buffer
                        IF (buf_ptr == lit(7, CMD_BUF_MAX)) {
                            state <= lit(5, ST_NORMAL);
                            cmd_ready_r <= 1'b0;
                        } ELSE {
                            cmd_buf.wr[buf_ptr] <= data;
                            buf_ptr <= buf_ptr + 7'd1;
                            cmd_ready_r <= 1'b0;

                            IF (data == lit(8, CHAR_SEMI)) {
                                // Semicolon — process completed parameter for SGR
                                param1   <= param_val;
                                has_semi <= 1'b1;
                                SELECT (param_val) {
                                    CASE (8'd0)   { pend_fg_idx <= 4'd7; pend_bg_idx <= 4'd0; pend_underline <= 1'b0; pend_slow_blink <= 1'b0; pend_fast_blink <= 1'b0; pend_inverse <= 1'b0; pend_conceal <= 1'b0; pend_strikethrough <= 1'b0; }
                                    CASE (8'd4)   { pend_underline <= 1'b1; }
                                    CASE (8'd5)   { pend_slow_blink <= 1'b1; }
                                    CASE (8'd6)   { pend_fast_blink <= 1'b1; }
                                    CASE (8'd7)   { pend_inverse <= 1'b1; }
                                    CASE (8'd8)   { pend_conceal <= 1'b1; }
                                    CASE (8'd9)   { pend_strikethrough <= 1'b1; }
                                    CASE (8'd24)  { pend_underline <= 1'b0; }
                                    CASE (8'd25)  { pend_slow_blink <= 1'b0; pend_fast_blink <= 1'b0; }
                                    CASE (8'd27)  { pend_inverse <= 1'b0; }
                                    CASE (8'd28)  { pend_conceal <= 1'b0; }
                                    CASE (8'd29)  { pend_strikethrough <= 1'b0; }
                                    CASE (8'd30)  { pend_fg_idx <= 4'd0; }
                                    CASE (8'd31)  { pend_fg_idx <= 4'd1; }
                                    CASE (8'd32)  { pend_fg_idx <= 4'd2; }
                                    CASE (8'd33)  { pend_fg_idx <= 4'd3; }
                                    CASE (8'd34)  { pend_fg_idx <= 4'd4; }
                                    CASE (8'd35)  { pend_fg_idx <= 4'd5; }
                                    CASE (8'd36)  { pend_fg_idx <= 4'd6; }
                                    CASE (8'd37)  { pend_fg_idx <= 4'd7; }
                                    CASE (8'd40)  { pend_bg_idx <= 4'd0; }
                                    CASE (8'd41)  { pend_bg_idx <= 4'd1; }
                                    CASE (8'd42)  { pend_bg_idx <= 4'd2; }
                                    CASE (8'd43)  { pend_bg_idx <= 4'd3; }
                                    CASE (8'd44)  { pend_bg_idx <= 4'd4; }
                                    CASE (8'd45)  { pend_bg_idx <= 4'd5; }
                                    CASE (8'd46)  { pend_bg_idx <= 4'd6; }
                                    CASE (8'd47)  { pend_bg_idx <= 4'd7; }
                                    CASE (8'd90)  { pend_fg_idx <= 4'd8; }
                                    CASE (8'd91)  { pend_fg_idx <= 4'd9; }
                                    CASE (8'd92)  { pend_fg_idx <= 4'd10; }
                                    CASE (8'd93)  { pend_fg_idx <= 4'd11; }
                                    CASE (8'd94)  { pend_fg_idx <= 4'd12; }
                                    CASE (8'd95)  { pend_fg_idx <= 4'd13; }
                                    CASE (8'd96)  { pend_fg_idx <= 4'd14; }
                                    CASE (8'd97)  { pend_fg_idx <= 4'd15; }
                                    CASE (8'd100) { pend_bg_idx <= 4'd8; }
                                    CASE (8'd101) { pend_bg_idx <= 4'd9; }
                                    CASE (8'd102) { pend_bg_idx <= 4'd10; }
                                    CASE (8'd103) { pend_bg_idx <= 4'd11; }
                                    CASE (8'd104) { pend_bg_idx <= 4'd12; }
                                    CASE (8'd105) { pend_bg_idx <= 4'd13; }
                                    CASE (8'd106) { pend_bg_idx <= 4'd14; }
                                    CASE (8'd107) { pend_bg_idx <= 4'd15; }
                                    DEFAULT { }
                                }
                                param_val <= 8'd0;
                            } ELIF (data == lit(8, CHAR_GT)) {
                                csi_gt <= 1'b1;
                            } ELIF (data == lit(8, CHAR_QUESTION)) {
                                csi_question <= 1'b1;
                            } ELSE {
                                // Digit — accumulate
                                IF (param_val >= 8'd26) {
                                    param_overflow <= 1'b1;
                                }
                                param_val <= param_x10 + { 4'b0000, data[3:0] };
                            }
                        }
                    } ELSE {
                        // Final byte — process last parameter for SGR, go to commit
                        SELECT (param_val) {
                            CASE (8'd0)   { pend_fg_idx <= 4'd7; pend_bg_idx <= 4'd0; pend_underline <= 1'b0; pend_slow_blink <= 1'b0; pend_fast_blink <= 1'b0; pend_inverse <= 1'b0; pend_conceal <= 1'b0; pend_strikethrough <= 1'b0; }
                            CASE (8'd4)   { pend_underline <= 1'b1; }
                            CASE (8'd5)   { pend_slow_blink <= 1'b1; }
                            CASE (8'd6)   { pend_fast_blink <= 1'b1; }
                            CASE (8'd7)   { pend_inverse <= 1'b1; }
                            CASE (8'd8)   { pend_conceal <= 1'b1; }
                            CASE (8'd9)   { pend_strikethrough <= 1'b1; }
                            CASE (8'd24)  { pend_underline <= 1'b0; }
                            CASE (8'd25)  { pend_slow_blink <= 1'b0; pend_fast_blink <= 1'b0; }
                            CASE (8'd27)  { pend_inverse <= 1'b0; }
                            CASE (8'd28)  { pend_conceal <= 1'b0; }
                            CASE (8'd29)  { pend_strikethrough <= 1'b0; }
                            CASE (8'd30)  { pend_fg_idx <= 4'd0; }
                            CASE (8'd31)  { pend_fg_idx <= 4'd1; }
                            CASE (8'd32)  { pend_fg_idx <= 4'd2; }
                            CASE (8'd33)  { pend_fg_idx <= 4'd3; }
                            CASE (8'd34)  { pend_fg_idx <= 4'd4; }
                            CASE (8'd35)  { pend_fg_idx <= 4'd5; }
                            CASE (8'd36)  { pend_fg_idx <= 4'd6; }
                            CASE (8'd37)  { pend_fg_idx <= 4'd7; }
                            CASE (8'd40)  { pend_bg_idx <= 4'd0; }
                            CASE (8'd41)  { pend_bg_idx <= 4'd1; }
                            CASE (8'd42)  { pend_bg_idx <= 4'd2; }
                            CASE (8'd43)  { pend_bg_idx <= 4'd3; }
                            CASE (8'd44)  { pend_bg_idx <= 4'd4; }
                            CASE (8'd45)  { pend_bg_idx <= 4'd5; }
                            CASE (8'd46)  { pend_bg_idx <= 4'd6; }
                            CASE (8'd47)  { pend_bg_idx <= 4'd7; }
                            CASE (8'd90)  { pend_fg_idx <= 4'd8; }
                            CASE (8'd91)  { pend_fg_idx <= 4'd9; }
                            CASE (8'd92)  { pend_fg_idx <= 4'd10; }
                            CASE (8'd93)  { pend_fg_idx <= 4'd11; }
                            CASE (8'd94)  { pend_fg_idx <= 4'd12; }
                            CASE (8'd95)  { pend_fg_idx <= 4'd13; }
                            CASE (8'd96)  { pend_fg_idx <= 4'd14; }
                            CASE (8'd97)  { pend_fg_idx <= 4'd15; }
                            CASE (8'd100) { pend_bg_idx <= 4'd8; }
                            CASE (8'd101) { pend_bg_idx <= 4'd9; }
                            CASE (8'd102) { pend_bg_idx <= 4'd10; }
                            CASE (8'd103) { pend_bg_idx <= 4'd11; }
                            CASE (8'd104) { pend_bg_idx <= 4'd12; }
                            CASE (8'd105) { pend_bg_idx <= 4'd13; }
                            CASE (8'd106) { pend_bg_idx <= 4'd14; }
                            CASE (8'd107) { pend_bg_idx <= 4'd15; }
                            DEFAULT { }
                        }
                        cmd_final_r <= data;
                        cmd_len_r   <= buf_ptr;
                        cmd_ready_r <= 1'b1;
                        state       <= lit(5, ST_CSI_COMMIT);
                    }
                }

                CASE (lit(5, ST_DCS)) {
                    // DCS: match incoming bytes against "EDID" and "TEST1"
                    do_write       <= 1'b0;
                    do_scroll      <= 1'b0;
                    do_scroll_down <= 1'b0;
                    do_scroll_reset <= 1'b0;
                    do_tx          <= 1'b0;
                    do_tb_rd_en    <= 1'b0;
                    cmd_ready_r    <= 1'b0;

                    IF (data == lit(8, CHAR_ESC)) {
                        state <= lit(5, ST_DCS_ESC);
                    } ELSE {
                        dcs_match_idx <= dcs_match_idx + 4'd1;
                        IF (data != dcs_expected) {
                            dcs_matched <= 1'b0;
                        }
                        // TEST1 match: "TEST1" = 0x54 0x45 0x53 0x54 0x31 (5 bytes)
                        SELECT (dcs_match_idx) {
                            CASE (4'd0) { IF (data != 8'h54) { dcs_test_matched <= 1'b0; } }
                            CASE (4'd1) { IF (data != 8'h45) { dcs_test_matched <= 1'b0; } }
                            CASE (4'd2) { IF (data != 8'h53) { dcs_test_matched <= 1'b0; } }
                            CASE (4'd3) { IF (data != 8'h54) { dcs_test_matched <= 1'b0; } }
                            CASE (4'd4) { IF (data != 8'h31) { dcs_test_matched <= 1'b0; } }
                            DEFAULT     { dcs_test_matched <= 1'b0; }
                        }
                    }
                }

                CASE (lit(5, ST_DCS_ESC)) {
                    // Got ESC inside DCS, check for '\' (ST terminator)
                    do_write       <= 1'b0;
                    do_scroll      <= 1'b0;
                    do_scroll_down <= 1'b0;
                    do_tx          <= 1'b0;
                    do_tb_rd_en    <= 1'b0;
                    cmd_ready_r    <= 1'b0;

                    IF (data == lit(8, CHAR_BACKSLASH)) {
                        IF (dcs_matched == 1'b1 && dcs_match_idx == 4'd4) {
                            // Valid EDID query
                            do_scroll_reset <= 1'b0;
                            resp_type  <= 2'd3;
                            resp_idx   <= 5'd0;
                            resp_len   <= 5'd23;
                            edid_phase <= 2'd0;
                            state      <= lit(5, ST_RESP_SEND);
                        } ELIF (dcs_test_matched == 1'b1 && dcs_match_idx == 4'd5) {
                            // Valid TEST1 query — clear screen then draw ruler
                            test_after_clear <= 1'b1;
                            clr_row     <= 6'd0;
                            clr_col     <= 8'd0;
                            clr_end_row <= lit(6, MAX_ROW);
                            clr_end_col <= lit(8, MAX_COL);
                            do_scroll_reset <= 1'b1;
                            state <= lit(5, ST_CLEAR);
                        } ELSE {
                            do_scroll_reset <= 1'b0;
                            state <= lit(5, ST_NORMAL);
                        }
                    } ELSE {
                        do_scroll_reset <= 1'b0;
                        state <= lit(5, ST_NORMAL);
                    }
                }

                CASE (lit(5, ST_OSC)) {
                    // OSC: consume bytes until BEL (0x07) or ESC (for ESC \)
                    do_write       <= 1'b0;
                    do_scroll      <= 1'b0;
                    do_scroll_down <= 1'b0;
                    do_scroll_reset <= 1'b0;
                    do_tx          <= 1'b0;
                    do_tb_rd_en    <= 1'b0;
                    cmd_ready_r    <= 1'b0;

                    IF (data == lit(8, CHAR_BEL)) {
                        // BEL terminates OSC
                        state <= lit(5, ST_NORMAL);
                    } ELIF (data == lit(8, CHAR_ESC)) {
                        // ESC inside OSC — check for '\'
                        state <= lit(5, ST_OSC_ESC);
                    }
                    // Otherwise stay in ST_OSC, consuming the byte
                }

                CASE (lit(5, ST_OSC_ESC)) {
                    // Got ESC inside OSC, check for '\' (ST terminator)
                    do_write       <= 1'b0;
                    do_scroll      <= 1'b0;
                    do_scroll_down <= 1'b0;
                    do_scroll_reset <= 1'b0;
                    do_tx          <= 1'b0;
                    do_tb_rd_en    <= 1'b0;
                    cmd_ready_r    <= 1'b0;

                    IF (data == lit(8, CHAR_BACKSLASH)) {
                        // ESC \ terminates OSC
                        state <= lit(5, ST_NORMAL);
                    } ELSE {
                        // Not '\' — back to OSC (treat ESC as consumed)
                        state <= lit(5, ST_OSC);
                    }
                }

                CASE (lit(5, ST_ESC_HASH)) {
                    // ESC # X — consume one byte and discard (e.g., ESC # 8 = DECALN)
                    do_write       <= 1'b0;
                    do_scroll      <= 1'b0;
                    do_scroll_down <= 1'b0;
                    do_scroll_reset <= 1'b0;
                    do_tx          <= 1'b0;
                    do_tb_rd_en    <= 1'b0;
                    cmd_ready_r    <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                }

                CASE (lit(5, ST_UTF8)) {
                    // Collecting UTF-8 continuation bytes
                    do_tx          <= 1'b0;
                    do_tb_rd_en    <= 1'b0;
                    cmd_ready_r    <= 1'b0;
                    do_scroll_down <= 1'b0;
                    do_scroll_reset <= 1'b0;

                    IF (data[7:6] == 2'b10) {
                        // Valid continuation byte — shift 6 bits in
                        IF (utf8_remain == 2'd1) {
                            // Last continuation byte — check for wide character
                            // Wide: U+3000-30FF (Hiragana/Katakana)
                            IF ({ utf8_accum[9:0], data[5:0] } >= 16'h3000 && { utf8_accum[9:0], data[5:0] } <= 16'h30FF) {
                                // Wide character — needs 2 cells
                                wide_char      <= { utf8_accum[9:0], data[5:0] };
                                use_write_attr <= 1'b0;

                                IF (cursor_col >= lit(8, MAX_COL)) {
                                    // No room for 2 cells — need to wrap first
                                    IF (autowrap == 1'b1) {
                                        cursor_col <= 8'd0;
                                        IF (cursor_row == lit(6, MAX_ROW)) {
                                            do_scroll <= 1'b1;
                                        } ELSE {
                                            cursor_row <= cursor_row + 6'd1;
                                            do_scroll  <= 1'b0;
                                        }
                                        // After wrap/scroll, write left half
                                        state <= lit(5, ST_WIDE_WRAP);
                                    } ELSE {
                                        // No autowrap — write left half at MAX_COL, no right half
                                        write_char <= { utf8_accum[9:0], data[5:0] };
                                        write_row  <= cursor_row;
                                        write_col  <= cursor_col;
                                        do_write   <= 1'b1;
                                        do_scroll  <= 1'b0;
                                        state      <= lit(5, ST_NORMAL);
                                    }
                                } ELSE {
                                    // Room available — write left half now
                                    write_char <= { utf8_accum[9:0], data[5:0] };
                                    write_row  <= cursor_row;
                                    write_col  <= cursor_col;
                                    do_write   <= 1'b1;
                                    do_scroll  <= 1'b0;
                                    state      <= lit(5, ST_WIDE_RIGHT);
                                }
                            // CJK Unified Ideographs: U+4E00-9FFF — remap via lookup
                            } ELIF ({ utf8_accum[9:0], data[5:0] } >= 16'h4E00 && { utf8_accum[9:0], data[5:0] } <= 16'h9FFF) {
                                // Start binary search to find Kanji slot
                                kanji_target   <= { utf8_accum[9:0], data[5:0] };
                                search_lo      <= 8'd0;
                                search_hi      <= 8'd228;  // max valid slot (229 entries - 1)
                                search_found   <= 1'b0;
                                do_write       <= 1'b0;
                                do_scroll      <= 1'b0;
                                use_write_attr <= 1'b0;
                                state          <= lit(5, ST_KANJI_READ);
                            } ELSE {
                                // Normal single-width character
                                write_char <= { utf8_accum[9:0], data[5:0] };
                                write_row  <= cursor_row;
                                write_col  <= cursor_col;
                                do_write   <= 1'b1;
                                use_write_attr <= 1'b0;
                                state      <= lit(5, ST_NORMAL);

                                IF (cursor_col == lit(8, MAX_COL)) {
                                    IF (autowrap == 1'b1) {
                                        cursor_col <= 8'd0;
                                        IF (cursor_row == lit(6, MAX_ROW)) {
                                            do_scroll <= 1'b1;
                                        } ELSE {
                                            cursor_row <= cursor_row + 6'd1;
                                            do_scroll <= 1'b0;
                                        }
                                    } ELSE {
                                        do_scroll <= 1'b0;
                                    }
                                } ELSE {
                                    cursor_col <= cursor_col + 8'd1;
                                    do_scroll <= 1'b0;
                                }
                            }
                        } ELSE {
                            // More continuation bytes expected
                            utf8_accum  <= { utf8_accum[9:0], data[5:0] };
                            utf8_remain <= utf8_remain - 2'd1;
                            do_write    <= 1'b0;
                            do_scroll   <= 1'b0;
                        }
                    } ELSE {
                        // Invalid byte in UTF-8 sequence — abandon
                        do_write <= 1'b0;
                        do_scroll <= 1'b0;
                        state <= lit(5, ST_NORMAL);
                    }
                }

                DEFAULT {
                    do_write  <= 1'b0;
                    do_scroll <= 1'b0;
                    do_scroll_down <= 1'b0;
                    do_scroll_reset <= 1'b0;
                    do_tx <= 1'b0;
                    do_tb_rd_en <= 1'b0;
                    cmd_ready_r <= 1'b0;
                    state <= lit(5, ST_NORMAL);
                }
            }
        } ELSE {
            do_write       <= 1'b0;
            do_scroll      <= 1'b0;
            do_scroll_down <= 1'b0;
            do_scroll_reset <= 1'b0;
            do_tx          <= 1'b0;
            do_tb_rd_en    <= 1'b0;
            cmd_ready_r    <= 1'b0;
            do_data_ack    <= 1'b0;
        }
    }
@endmod
jz
// Terminal Buffer: 142 columns x 45 rows, SDRAM-backed with double-buffered line cache
// Two BSRAM line buffers (256x32 each, using 142 entries).
// While one is read by the pixel generator, the other is prefetched from SDRAM.
// Swap when the text row changes.
//
// SDRAM addressing: {6'b0, use_alt, phys_row[5:0], col[7:0]} (21-bit, row stride = 256)
// Data packing: One 32-bit SDRAM word = {attr[15:0], char[15:0]}
//
// On startup, the entire 45x142 SDRAM region is cleared to blank cells before
// the terminal is allowed to operate (init_clear phase).
@module terminal_buffer
    CONST {
        MAX_ROW = 44;
        MAX_COL = 141;
        DEFAULT_ATTR = 28672;

        // State machine states
        ST_IDLE      = 0;
        ST_PREFETCH  = 1;
        ST_WRITE     = 2;
        ST_READBACK  = 3;
    }

    PORT {
        IN  [1] clk;
        IN  [1] rst_n;

        // Read port (pixel generator)
        IN  [6] rd_row;
        IN  [8] rd_col;
        OUT [16] char_code;
        OUT [16] attr_out;

        // Write port (terminal)
        IN  [6]  wr_row;
        IN  [8]  wr_col;
        IN  [16] wr_char;
        IN  [16] wr_attr;
        IN  [1]  wr_en;

        // Terminal read-back port (for insert mode)
        IN  [6]  tb_rd_row;
        IN  [8]  tb_rd_col;
        OUT [16] tb_rd_char;
        OUT [16] tb_rd_attr;
        IN  [1]  tb_rd_en;

        // Scroll interface
        IN  [1]  scroll;
        IN  [1]  scroll_down;
        IN  [1]  scroll_reset;
        OUT [1]  busy;

        // Alt screen select (from terminal)
        IN  [1]  use_alt;

        // Look-ahead row for prefetch
        IN  [6]  rd_row_next;

        // Display active (suppress active-buffer write-through to prevent tearing)
        IN  [1]  display_active;

        // High after init complete and first prefetch done
        OUT [1]  buf_ready;

        // SDRAM interface (directly to arbiter port)
        OUT [21] sd_addr;
        OUT [32] sd_wdata;
        OUT [1]  sd_rd;
        OUT [1]  sd_wr;
        IN  [32] sd_rdata;
        IN  [1]  sd_busy;
        IN  [1]  sd_done;
    }

    WIRE {
        // Physical row computation
        rd_phys_row    [6];
        wr_phys_row    [6];
        tb_rd_phys_row [6];
        next_phys_row  [6];
    }

    REGISTER {
        scroll_offset [6] = 6'd0;

        // Line buffer control
        active_buf    [1] = 1'b0;       // Which buffer pixel gen reads (0=A, 1=B)
        cached_row    [6] = 6'd63;      // Phys row currently in active buffer (63=invalid)
        prefetch_row  [6] = 6'd63;      // Phys row currently in prefetch buffer

        // State machine
        state         [2] = 2'd0;
        pf_col        [8] = 8'd0;       // Prefetch column counter
        pf_target_row [6] = 6'd0;       // Row being prefetched

        // SDRAM request registers
        sd_addr_r     [21] = 21'd0;
        sd_wdata_r    [32] = 32'd0;
        sd_rd_r       [1]  = 1'b0;
        sd_wr_r       [1]  = 1'b0;

        // Scroll/init line clear state
        clearing       [1]  = 1'b1;     // Start clearing on reset
        clear_col      [8]  = 8'd0;
        clear_phys_row [6]  = 6'd0;
        init_clear     [1]  = 1'b1;     // Full-screen clear on startup

        // Track need for prefetch
        need_prefetch [1]  = 1'b0;

        // Pixel gen output registers
        char_out      [16] = 16'h0000;
        attr_out_r    [16] = 16'h7000;

        // Line buffer read pipeline
        lb_bank_sel   [1]  = 1'b0;

        // Read-back output registers
        tb_rd_char_r  [16] = 16'h0000;
        tb_rd_attr_r  [16] = 16'h7000;

        // Line buffer write staging (single write site uses these next cycle)
        lb_wr_en      [1]  = 1'b0;
        lb_wr_to_a    [1]  = 1'b0;
        lb_wr_addr_r  [8]  = 8'd0;
        lb_wr_data_r  [32] = 32'd0;

        // Pending write (latched when terminal writes while not idle)
        pend_wr       [1]  = 1'b0;
        pend_wr_row   [6]  = 6'd0;
        pend_wr_col   [8]  = 8'd0;
        pend_wr_char  [16] = 16'h0000;
        pend_wr_attr  [16] = 16'h7000;

        // Buffer ready (set after init_clear + first prefetch)
        buf_ready_r   [1]  = 1'b0;

        // Pending read-back
        pend_rb       [1]  = 1'b0;
        pend_rb_row   [6]  = 6'd0;
        pend_rb_col   [8]  = 8'd0;
    }

    MEM(TYPE=BLOCK) {
        // Line buffer A: 32-bit wide x 256 deep (uses 142 entries)
        // Stores {attr[15:0], char[15:0]}
        line_a [32] [256] = 32'h70000000 { OUT rd SYNC; IN wr; };

        // Line buffer B
        line_b [32] [256] = 32'h70000000 { OUT rd SYNC; IN wr; };
    }

    ASYNCHRONOUS {
        // Map logical rows to physical rows via scroll offset (mod 64)
        rd_phys_row    <= rd_row + scroll_offset;
        wr_phys_row    <= wr_row + scroll_offset;
        tb_rd_phys_row <= tb_rd_row + scroll_offset;
        next_phys_row  <= rd_row_next + scroll_offset;

        // SDRAM interface — mask with ~sd_done to prevent double-accept
        // when SDRAM arbiter returns done on the same cycle
        sd_addr  <= sd_addr_r;
        sd_wdata <= sd_wdata_r;
        sd_rd    <= sd_rd_r & ~sd_done;
        sd_wr    <= sd_wr_r & ~sd_done;

        // Output char/attr from registered values
        char_code <= char_out;
        attr_out  <= attr_out_r;

        // Terminal read-back outputs
        tb_rd_char <= tb_rd_char_r;
        tb_rd_attr <= tb_rd_attr_r;

        // Busy: prevents terminal from issuing new operations.
        // Feed-forward terms (wr_en & ~pend_wr) / (tb_rd_en & ~pend_rb) make busy
        // go high on the same cycle a write/read-back is accepted into the pending
        // slot, closing the 1-cycle NBA window that could drop a second request.
        busy <= clearing | pend_wr | pend_rb
              | (state == lit(2, ST_WRITE)) | (state == lit(2, ST_READBACK))
              | (wr_en & ~pend_wr) | (tb_rd_en & ~pend_rb);

        // Buffer ready for display
        buf_ready <= buf_ready_r;
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        // === Line buffer read path (always runs for pixel generator) ===
        line_a.rd.addr <= rd_col;
        line_b.rd.addr <= rd_col;
        lb_bank_sel    <= active_buf;

        // Capture line buffer output (mux based on registered bank select)
        IF (lb_bank_sel == 1'b0) {
            char_out   <= line_a.rd.data[15:0];
            attr_out_r <= line_a.rd.data[31:16];
        } ELSE {
            char_out   <= line_b.rd.data[15:0];
            attr_out_r <= line_b.rd.data[31:16];
        }

        // === Single line buffer write site (from staged values) ===
        IF (lb_wr_en == 1'b1 && lb_wr_to_a == 1'b1) {
            line_a.wr[lb_wr_addr_r] <= lb_wr_data_r;
        } ELIF (lb_wr_en == 1'b1 && lb_wr_to_a == 1'b0) {
            line_b.wr[lb_wr_addr_r] <= lb_wr_data_r;
        }

        // === Unified state machine ===
        SELECT (state) {
            CASE (lit(2, ST_IDLE)) {
                // --- Latch incoming requests when they can't be handled directly ---
                // (wr_en/tb_rd_en arrive here only if we process them directly;
                //  in non-IDLE states they're latched in those CASEs below)

                // --- Priority-ordered dispatch ---
                IF (scroll == 1'b1 && clearing == 1'b0 && use_alt == 1'b0) {
                    // Scroll up: advance offset, start clearing new bottom row
                    scroll_offset  <= scroll_offset + 6'd1;
                    clearing       <= 1'b1;
                    clear_col      <= 8'd0;
                    clear_phys_row <= lit(6, MAX_ROW) + scroll_offset + 6'd1;
                    cached_row     <= 6'd63;
                    prefetch_row   <= 6'd63;
                    need_prefetch  <= 1'b1;
                    sd_rd_r  <= 1'b0;
                    sd_wr_r  <= 1'b0;
                    lb_wr_en <= 1'b0;
                } ELIF (scroll_down == 1'b1 && clearing == 1'b0 && use_alt == 1'b0) {
                    // Scroll down: decrement offset, clear new top row
                    scroll_offset  <= scroll_offset - 6'd1;
                    clearing       <= 1'b1;
                    clear_col      <= 8'd0;
                    clear_phys_row <= scroll_offset - 6'd1;
                    cached_row     <= 6'd63;
                    prefetch_row   <= 6'd63;
                    need_prefetch  <= 1'b1;
                    sd_rd_r  <= 1'b0;
                    sd_wr_r  <= 1'b0;
                    lb_wr_en <= 1'b0;
                } ELIF (scroll_reset == 1'b1 && clearing == 1'b0 && use_alt == 1'b0) {
                    // Reset scroll offset
                    scroll_offset <= 6'd0;
                    cached_row    <= 6'd63;
                    prefetch_row  <= 6'd63;
                    need_prefetch <= 1'b1;
                    sd_rd_r  <= 1'b0;
                    sd_wr_r  <= 1'b0;
                    lb_wr_en <= 1'b0;
                } ELIF (clearing == 1'b1 && sd_busy == 1'b0) {
                    // Clear one cell via SDRAM write
                    sd_addr_r  <= { 6'b000000, use_alt, clear_phys_row, clear_col };
                    sd_wdata_r <= { 16'h7000, 16'h0000 };
                    sd_wr_r    <= 1'b1;
                    sd_rd_r    <= 1'b0;
                    state      <= lit(2, ST_WRITE);

                    // Write-through to line buffer if row matches cache
                    // Skip active buffer during display to prevent tearing
                    IF (clear_phys_row == cached_row && display_active == 1'b0) {
                        lb_wr_en     <= 1'b1;
                        lb_wr_to_a   <= ~active_buf;
                        lb_wr_addr_r <= clear_col;
                        lb_wr_data_r <= { 16'h7000, 16'h0000 };
                    } ELIF (clear_phys_row == prefetch_row) {
                        lb_wr_en     <= 1'b1;
                        lb_wr_to_a   <= active_buf;
                        lb_wr_addr_r <= clear_col;
                        lb_wr_data_r <= { 16'h7000, 16'h0000 };
                    } ELSE {
                        lb_wr_en <= 1'b0;
                    }

                    // Advance clear position
                    IF (clear_col == lit(8, MAX_COL)) {
                        IF (init_clear == 1'b1 && clear_phys_row != lit(6, MAX_ROW)) {
                            // More rows to clear during startup init
                            clear_col      <= 8'd0;
                            clear_phys_row <= clear_phys_row + 6'd1;
                        } ELSE {
                            // Done clearing (single row or last init row)
                            clearing   <= 1'b0;
                            init_clear <= 1'b0;
                        }
                    } ELSE {
                        clear_col <= clear_col + 8'd1;
                    }
                } ELIF (pend_wr == 1'b1 && sd_busy == 1'b0 && clearing == 1'b0) {
                    // Process pending terminal write
                    sd_addr_r  <= { 6'b000000, use_alt, pend_wr_row, pend_wr_col };
                    sd_wdata_r <= { pend_wr_attr, pend_wr_char };
                    sd_wr_r    <= 1'b1;
                    sd_rd_r    <= 1'b0;
                    pend_wr    <= 1'b0;
                    state      <= lit(2, ST_WRITE);

                    // Write-through to line buffer if row matches cache
                    // Skip active buffer during display to prevent tearing
                    IF (pend_wr_row == cached_row && display_active == 1'b0) {
                        lb_wr_en     <= 1'b1;
                        lb_wr_to_a   <= ~active_buf;
                        lb_wr_addr_r <= pend_wr_col;
                        lb_wr_data_r <= { pend_wr_attr, pend_wr_char };
                    } ELIF (pend_wr_row == prefetch_row) {
                        lb_wr_en     <= 1'b1;
                        lb_wr_to_a   <= active_buf;
                        lb_wr_addr_r <= pend_wr_col;
                        lb_wr_data_r <= { pend_wr_attr, pend_wr_char };
                    } ELSE {
                        lb_wr_en <= 1'b0;
                    }
                } ELIF (pend_rb == 1'b1 && sd_busy == 1'b0 && clearing == 1'b0) {
                    // Process pending read-back
                    sd_addr_r <= { 6'b000000, use_alt, pend_rb_row, pend_rb_col };
                    sd_rd_r   <= 1'b1;
                    sd_wr_r   <= 1'b0;
                    pend_rb   <= 1'b0;
                    lb_wr_en  <= 1'b0;
                    state     <= lit(2, ST_READBACK);
                } ELIF (wr_en == 1'b1 && sd_busy == 1'b0 && clearing == 1'b0 && pend_wr == 1'b0) {
                    // Direct terminal write (no pending, SDRAM ready)
                    sd_addr_r  <= { 6'b000000, use_alt, wr_phys_row, wr_col };
                    sd_wdata_r <= { wr_attr, wr_char };
                    sd_wr_r    <= 1'b1;
                    sd_rd_r    <= 1'b0;
                    state      <= lit(2, ST_WRITE);

                    // Write-through to line buffer if row matches cache
                    // Skip active buffer during display to prevent tearing
                    IF (wr_phys_row == cached_row && display_active == 1'b0) {
                        lb_wr_en     <= 1'b1;
                        lb_wr_to_a   <= ~active_buf;
                        lb_wr_addr_r <= wr_col;
                        lb_wr_data_r <= { wr_attr, wr_char };
                    } ELIF (wr_phys_row == prefetch_row) {
                        lb_wr_en     <= 1'b1;
                        lb_wr_to_a   <= active_buf;
                        lb_wr_addr_r <= wr_col;
                        lb_wr_data_r <= { wr_attr, wr_char };
                    } ELSE {
                        lb_wr_en <= 1'b0;
                    }
                } ELIF (wr_en == 1'b1 && pend_wr == 1'b0) {
                    // Latch write: SDRAM busy or clearing, save for later
                    pend_wr      <= 1'b1;
                    pend_wr_row  <= wr_phys_row;
                    pend_wr_col  <= wr_col;
                    pend_wr_char <= wr_char;
                    pend_wr_attr <= wr_attr;
                    sd_rd_r  <= 1'b0;
                    sd_wr_r  <= 1'b0;
                    lb_wr_en <= 1'b0;
                } ELIF (tb_rd_en == 1'b1 && sd_busy == 1'b0 && clearing == 1'b0 && pend_rb == 1'b0) {
                    // Direct read-back
                    sd_addr_r <= { 6'b000000, use_alt, tb_rd_phys_row, tb_rd_col };
                    sd_rd_r   <= 1'b1;
                    sd_wr_r   <= 1'b0;
                    lb_wr_en  <= 1'b0;
                    state     <= lit(2, ST_READBACK);
                } ELIF (tb_rd_en == 1'b1 && pend_rb == 1'b0) {
                    // Latch read-back
                    pend_rb     <= 1'b1;
                    pend_rb_row <= tb_rd_phys_row;
                    pend_rb_col <= tb_rd_col;
                    sd_rd_r  <= 1'b0;
                    sd_wr_r  <= 1'b0;
                    lb_wr_en <= 1'b0;
                } ELIF (rd_phys_row != cached_row && rd_phys_row == prefetch_row) {
                    // Buffer swap: prefetch buffer has the row we need
                    active_buf   <= ~active_buf;
                    cached_row   <= prefetch_row;
                    prefetch_row <= cached_row;
                    sd_rd_r  <= 1'b0;
                    sd_wr_r  <= 1'b0;
                    lb_wr_en <= 1'b0;
                } ELIF (need_prefetch == 1'b1 && sd_busy == 1'b0 && clearing == 1'b0) {
                    // Start prefetching into inactive buffer
                    // Prefer current row if not cached (urgent), else next row
                    pf_col        <= 8'd0;
                    need_prefetch <= 1'b0;
                    IF (rd_phys_row != cached_row && rd_phys_row != prefetch_row) {
                        pf_target_row <= rd_phys_row;
                        sd_addr_r <= { 6'b000000, use_alt, rd_phys_row, 8'd0 };
                    } ELSE {
                        pf_target_row <= next_phys_row;
                        sd_addr_r <= { 6'b000000, use_alt, next_phys_row, 8'd0 };
                    }
                    sd_rd_r   <= 1'b1;
                    sd_wr_r   <= 1'b0;
                    lb_wr_en  <= 1'b0;
                    state     <= lit(2, ST_PREFETCH);
                } ELSE {
                    sd_rd_r  <= 1'b0;
                    sd_wr_r  <= 1'b0;
                    lb_wr_en <= 1'b0;

                    // Detect prefetch need when truly idle
                    // Current row takes priority (urgent), then look-ahead
                    IF (rd_phys_row != cached_row && rd_phys_row != prefetch_row && need_prefetch == 1'b0) {
                        need_prefetch <= 1'b1;
                    } ELIF (next_phys_row != prefetch_row && next_phys_row != cached_row && need_prefetch == 1'b0) {
                        need_prefetch <= 1'b1;
                    }
                }
            }

            CASE (lit(2, ST_WRITE)) {
                sd_rd_r  <= 1'b0;
                lb_wr_en <= 1'b0;
                // sd_wr_r: hold at 1 until sd_done (don't clear unconditionally)

                // Latch incoming write while we're busy with SDRAM
                IF (wr_en == 1'b1 && pend_wr == 1'b0) {
                    pend_wr      <= 1'b1;
                    pend_wr_row  <= wr_phys_row;
                    pend_wr_col  <= wr_col;
                    pend_wr_char <= wr_char;
                    pend_wr_attr <= wr_attr;
                }

                // Latch incoming read-back
                IF (tb_rd_en == 1'b1 && pend_rb == 1'b0) {
                    pend_rb     <= 1'b1;
                    pend_rb_row <= tb_rd_phys_row;
                    pend_rb_col <= tb_rd_col;
                }

                IF (sd_done == 1'b1) {
                    sd_wr_r <= 1'b0;
                    state   <= lit(2, ST_IDLE);
                }
            }

            CASE (lit(2, ST_READBACK)) {
                sd_wr_r  <= 1'b0;
                lb_wr_en <= 1'b0;
                // sd_rd_r: hold at 1 until sd_done (don't clear unconditionally)

                // Latch incoming write while we're busy
                IF (wr_en == 1'b1 && pend_wr == 1'b0) {
                    pend_wr      <= 1'b1;
                    pend_wr_row  <= wr_phys_row;
                    pend_wr_col  <= wr_col;
                    pend_wr_char <= wr_char;
                    pend_wr_attr <= wr_attr;
                }

                // Latch incoming read-back
                IF (tb_rd_en == 1'b1 && pend_rb == 1'b0) {
                    pend_rb     <= 1'b1;
                    pend_rb_row <= tb_rd_phys_row;
                    pend_rb_col <= tb_rd_col;
                }

                IF (sd_done == 1'b1) {
                    sd_rd_r      <= 1'b0;
                    tb_rd_char_r <= sd_rdata[15:0];
                    tb_rd_attr_r <= sd_rdata[31:16];
                    state        <= lit(2, ST_IDLE);
                }
            }

            CASE (lit(2, ST_PREFETCH)) {
                // Latch incoming write while prefetching
                IF (wr_en == 1'b1 && pend_wr == 1'b0) {
                    pend_wr      <= 1'b1;
                    pend_wr_row  <= wr_phys_row;
                    pend_wr_col  <= wr_col;
                    pend_wr_char <= wr_char;
                    pend_wr_attr <= wr_attr;
                }

                // Latch incoming read-back
                IF (tb_rd_en == 1'b1 && pend_rb == 1'b0) {
                    pend_rb     <= 1'b1;
                    pend_rb_row <= tb_rd_phys_row;
                    pend_rb_col <= tb_rd_col;
                }

                IF (sd_done == 1'b1) {
                    // Stage line buffer write for inactive buffer
                    lb_wr_en     <= 1'b1;
                    lb_wr_to_a   <= active_buf;
                    lb_wr_addr_r <= pf_col;
                    lb_wr_data_r <= sd_rdata;

                    IF (pf_col == lit(8, MAX_COL)) {
                        // Prefetch complete
                        prefetch_row <= pf_target_row;
                        buf_ready_r  <= 1'b1;
                        sd_rd_r      <= 1'b0;
                        sd_wr_r      <= 1'b0;
                        state        <= lit(2, ST_IDLE);
                    } ELSE {
                        // Issue next SDRAM read
                        pf_col    <= pf_col + 8'd1;
                        sd_addr_r <= { 6'b000000, use_alt, pf_target_row, pf_col + 8'd1 };
                        sd_rd_r   <= 1'b1;
                        sd_wr_r   <= 1'b0;
                    }
                } ELSE {
                    // sd_rd_r: hold at 1 (don't clear — keeps read request
                    // asserted until SDRAM accepts it, surviving refresh cycles)
                    sd_wr_r  <= 1'b0;
                    lb_wr_en <= 1'b0;
                }
            }

            DEFAULT {
                sd_rd_r  <= 1'b0;
                sd_wr_r  <= 1'b0;
                lb_wr_en <= 1'b0;
                state    <= lit(2, ST_IDLE);
            }
        }
    }
@endmod
jz
// Pixel Generator for text-mode terminal display
// 142 columns x 45 rows of 9x16 pixel character cells on 1280x720 display
// 4-cycle pipeline: line buf BSRAM (1) -> bank mux (2) -> font BSRAM (3) -> font bank mux (4)
// Control signals (de, hsync, vsync) delayed 4 cycles to match pixel data
//
// ANSI 16-color palette with attribute support:
//   Attribute word [15:12]=FG index, [11:8]=BG index, [7]=underline,
//   [6]=slow_blink, [5]=fast_blink, [4]=strikethrough
//
// Blink timing: 27-bit free-running counter; bit 26 = slow blink, bit 25 = fast blink
//
// Text area: 142 cols * 9 px = 1278 px, centered with 1px borders (x=1..1278)
@module pixel_generator
    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;
        IN  [11] x_pos;
        IN  [10] y_pos;
        IN  [1]  display_enable;
        IN  [1]  hsync;
        IN  [1]  vsync;

        // Terminal buffer read interface
        OUT [6]  rd_row;
        OUT [8]  rd_col;
        IN  [16] buf_char;
        IN  [16] buf_attr;
        IN  [1]  buf_ready;

        // Look-ahead row for prefetch
        OUT [6]  rd_row_next;

        OUT [8]  red;
        OUT [8]  green;
        OUT [8]  blue;
        OUT [1]  de_out;
        OUT [1]  hsync_out;
        OUT [1]  vsync_out;

        // Font cache SDRAM interface (passed through to arbiter)
        OUT [21] font_sd_addr;
        OUT [1]  font_sd_rd;
        IN  [32] font_sd_rdata;
        IN  [1]  font_sd_busy;
        IN  [1]  font_sd_done;

        // Font cache ready (from decompressor completion)
        IN  [1]  font_ready;
    }

    WIRE {
        // Font RAM interface
        char_row      [4];
        font_bitmap   [8];
        font_graphic  [1];

        // Pixel computation
        pixel_on      [1];

        // ANSI color palette lookup
        fg_red   [8];
        fg_green [8];
        fg_blue  [8];
        bg_red   [8];
        bg_green [8];
        bg_blue  [8];

        // Next text row
        next_text_row [6];
    }

    REGISTER {
        // Character cell counters
        pixel_x   [4] = 4'd0;
        text_col  [8] = 8'd0;
        in_text   [1] = 1'b0;

        // Pipeline stage 1
        p1_pixel_x  [4] = 4'd0;
        p1_de       [1] = 1'b0;
        p1_hsync    [1] = 1'b0;
        p1_vsync    [1] = 1'b0;
        p1_in_text  [1] = 1'b0;

        // Pipeline stage 2
        p2_pixel_x  [4] = 4'd0;
        p2_de       [1] = 1'b0;
        p2_hsync    [1] = 1'b0;
        p2_vsync    [1] = 1'b0;
        p2_in_text  [1] = 1'b0;

        // Pipeline stage 3 (captures vram output + attr decode)
        p3_pixel_x     [4] = 4'd0;
        p3_fg_idx      [4] = 4'd7;
        p3_bg_idx      [4] = 4'd0;
        p3_underline   [1] = 1'b0;
        p3_slow_blink  [1] = 1'b0;
        p3_fast_blink  [1] = 1'b0;
        p3_strike      [1] = 1'b0;
        p3_graphic     [1] = 1'b0;
        p3_char_row    [4] = 4'd0;
        p3_de          [1] = 1'b0;
        p3_hsync       [1] = 1'b0;
        p3_vsync       [1] = 1'b0;
        p3_in_text     [1] = 1'b0;

        // Pipeline stage 4 (font output ready)
        p4_pixel_x     [4] = 4'd0;
        p4_fg_idx      [4] = 4'd7;
        p4_bg_idx      [4] = 4'd0;
        p4_underline   [1] = 1'b0;
        p4_slow_blink  [1] = 1'b0;
        p4_fast_blink  [1] = 1'b0;
        p4_strike      [1] = 1'b0;
        p4_graphic     [1] = 1'b0;
        p4_char_row    [4] = 4'd0;
        p4_de          [1] = 1'b0;
        p4_hsync       [1] = 1'b0;
        p4_vsync       [1] = 1'b0;
        p4_in_text     [1] = 1'b0;

        // Blink counter (free-running; bit 26 = slow blink, bit 25 = fast blink)
        blink_cnt [27] = 27'd0;
    }

    @new font0 font_cache {
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = rst_n;
        IN  [16] char_code      = buf_char;
        IN  [4]  row            = char_row;
        OUT [8]  bitmap         = font_bitmap;
        OUT [1]  graphic        = font_graphic;
        IN  [4]  char_row       = char_row;
        IN  [8]  text_col       = text_col;
        IN  [16] pf_char        = buf_char;
        IN  [1]  font_ready     = font_ready;
        IN  [1]  display_active = display_enable;
        OUT [21] sd_addr        = font_sd_addr;
        OUT [1]  sd_rd          = font_sd_rd;
        IN  [32] sd_rdata       = font_sd_rdata;
        IN  [1]  sd_busy        = font_sd_busy;
        IN  [1]  sd_done        = font_sd_done;
    }

    ASYNCHRONOUS {
        // Drive terminal buffer read address
        // Clamp to valid row range (0-44) during vertical blanking (y >= 720)
        // to prevent prefetching uninitialized SDRAM rows 45+
        IF (y_pos[9:4] > 6'd44) {
            rd_row <= 6'd0;
        } ELSE {
            rd_row <= y_pos[9:4];
        }
        rd_col   <= text_col;
        char_row <= y_pos[3:0];

        // Look-ahead: next text row for prefetch
        IF (y_pos[9:4] >= 6'd44) {
            next_text_row <= 6'd0;
        } ELSE {
            next_text_row <= y_pos[9:4] + 6'd1;
        }
        rd_row_next <= next_text_row;

        // Delayed control signals (4 cycles to match pixel data)
        de_out    <= p4_de;
        hsync_out <= p4_hsync;
        vsync_out <= p4_vsync;

        // Pixel selection from font bitmap with attribute overrides
        IF (p4_de == 1'b0 || p4_in_text == 1'b0) {
            pixel_on <= 1'b0;
        } ELIF (p4_fast_blink == 1'b1 && blink_cnt[24] == 1'b0) {
            pixel_on <= 1'b0;
        } ELIF (p4_slow_blink == 1'b1 && blink_cnt[26] == 1'b0) {
            pixel_on <= 1'b0;
        } ELIF (p4_underline == 1'b1 && p4_char_row == 4'd14) {
            pixel_on <= 1'b1;
        } ELIF (p4_strike == 1'b1 && p4_char_row == 4'd7) {
            pixel_on <= 1'b1;
        } ELSE {
            SELECT (p4_pixel_x) {
                CASE (4'd0) { pixel_on <= font_bitmap[7]; }
                CASE (4'd1) { pixel_on <= font_bitmap[6]; }
                CASE (4'd2) { pixel_on <= font_bitmap[5]; }
                CASE (4'd3) { pixel_on <= font_bitmap[4]; }
                CASE (4'd4) { pixel_on <= font_bitmap[3]; }
                CASE (4'd5) { pixel_on <= font_bitmap[2]; }
                CASE (4'd6) { pixel_on <= font_bitmap[1]; }
                CASE (4'd7) { pixel_on <= font_bitmap[0]; }
                CASE (4'd8) {
                    pixel_on <= (p4_graphic == 1'b1) ? font_bitmap[0] : 1'b0;
                }
                DEFAULT { pixel_on <= 1'b0; }
            }
        }

        // ANSI 16-color palette lookup for foreground
        SELECT (p4_fg_idx) {
            CASE (4'd0)  { fg_red <= 8'h00; fg_green <= 8'h00; fg_blue <= 8'h00; }
            CASE (4'd1)  { fg_red <= 8'hAA; fg_green <= 8'h00; fg_blue <= 8'h00; }
            CASE (4'd2)  { fg_red <= 8'h00; fg_green <= 8'hAA; fg_blue <= 8'h00; }
            CASE (4'd3)  { fg_red <= 8'hAA; fg_green <= 8'h55; fg_blue <= 8'h00; }
            CASE (4'd4)  { fg_red <= 8'h00; fg_green <= 8'h00; fg_blue <= 8'hAA; }
            CASE (4'd5)  { fg_red <= 8'hAA; fg_green <= 8'h00; fg_blue <= 8'hAA; }
            CASE (4'd6)  { fg_red <= 8'h00; fg_green <= 8'hAA; fg_blue <= 8'hAA; }
            CASE (4'd7)  { fg_red <= 8'hAA; fg_green <= 8'hAA; fg_blue <= 8'hAA; }
            CASE (4'd8)  { fg_red <= 8'h55; fg_green <= 8'h55; fg_blue <= 8'h55; }
            CASE (4'd9)  { fg_red <= 8'hFF; fg_green <= 8'h55; fg_blue <= 8'h55; }
            CASE (4'd10) { fg_red <= 8'h55; fg_green <= 8'hFF; fg_blue <= 8'h55; }
            CASE (4'd11) { fg_red <= 8'hFF; fg_green <= 8'hFF; fg_blue <= 8'h55; }
            CASE (4'd12) { fg_red <= 8'h55; fg_green <= 8'h55; fg_blue <= 8'hFF; }
            CASE (4'd13) { fg_red <= 8'hFF; fg_green <= 8'h55; fg_blue <= 8'hFF; }
            CASE (4'd14) { fg_red <= 8'h55; fg_green <= 8'hFF; fg_blue <= 8'hFF; }
            CASE (4'd15) { fg_red <= 8'hFF; fg_green <= 8'hFF; fg_blue <= 8'hFF; }
            DEFAULT      { fg_red <= 8'hAA; fg_green <= 8'hAA; fg_blue <= 8'hAA; }
        }

        // ANSI 16-color palette lookup for background
        SELECT (p4_bg_idx) {
            CASE (4'd0)  { bg_red <= 8'h00; bg_green <= 8'h00; bg_blue <= 8'h00; }
            CASE (4'd1)  { bg_red <= 8'hAA; bg_green <= 8'h00; bg_blue <= 8'h00; }
            CASE (4'd2)  { bg_red <= 8'h00; bg_green <= 8'hAA; bg_blue <= 8'h00; }
            CASE (4'd3)  { bg_red <= 8'hAA; bg_green <= 8'h55; bg_blue <= 8'h00; }
            CASE (4'd4)  { bg_red <= 8'h00; bg_green <= 8'h00; bg_blue <= 8'hAA; }
            CASE (4'd5)  { bg_red <= 8'hAA; bg_green <= 8'h00; bg_blue <= 8'hAA; }
            CASE (4'd6)  { bg_red <= 8'h00; bg_green <= 8'hAA; bg_blue <= 8'hAA; }
            CASE (4'd7)  { bg_red <= 8'hAA; bg_green <= 8'hAA; bg_blue <= 8'hAA; }
            CASE (4'd8)  { bg_red <= 8'h55; bg_green <= 8'h55; bg_blue <= 8'h55; }
            CASE (4'd9)  { bg_red <= 8'hFF; bg_green <= 8'h55; bg_blue <= 8'h55; }
            CASE (4'd10) { bg_red <= 8'h55; bg_green <= 8'hFF; bg_blue <= 8'h55; }
            CASE (4'd11) { bg_red <= 8'hFF; bg_green <= 8'hFF; bg_blue <= 8'h55; }
            CASE (4'd12) { bg_red <= 8'h55; bg_green <= 8'h55; bg_blue <= 8'hFF; }
            CASE (4'd13) { bg_red <= 8'hFF; bg_green <= 8'h55; bg_blue <= 8'hFF; }
            CASE (4'd14) { bg_red <= 8'h55; bg_green <= 8'hFF; bg_blue <= 8'hFF; }
            CASE (4'd15) { bg_red <= 8'hFF; bg_green <= 8'hFF; bg_blue <= 8'hFF; }
            DEFAULT      { bg_red <= 8'h00; bg_green <= 8'h00; bg_blue <= 8'h00; }
        }

        // Color output from palette lookup
        IF (p4_de == 1'b0 || buf_ready == 1'b0) {
            red   <= 8'b0;
            green <= 8'b0;
            blue  <= 8'b0;
        } ELIF (p4_in_text == 1'b0) {
            red   <= 8'b0;
            green <= 8'b0;
            blue  <= 8'b0;
        } ELIF (pixel_on == 1'b1) {
            red   <= fg_red;
            green <= fg_green;
            blue  <= fg_blue;
        } ELSE {
            red   <= bg_red;
            green <= bg_green;
            blue  <= bg_blue;
        }
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        // Pipeline stage 1: capture counters and control
        p1_pixel_x <= pixel_x;
        p1_de      <= display_enable;
        p1_hsync   <= hsync;
        p1_vsync   <= vsync;
        p1_in_text <= in_text;

        // Pipeline stage 2: pass through
        p2_pixel_x <= p1_pixel_x;
        p2_de      <= p1_de;
        p2_hsync   <= p1_hsync;
        p2_vsync   <= p1_vsync;
        p2_in_text <= p1_in_text;

        // Pipeline stage 3: pass through + capture vram output + decode attributes
        p3_pixel_x    <= p2_pixel_x;
        p3_de         <= p2_de;
        p3_hsync      <= p2_hsync;
        p3_vsync      <= p2_vsync;
        p3_in_text    <= p2_in_text;
        p3_fg_idx     <= buf_attr[15:12];
        p3_bg_idx     <= buf_attr[11:8];
        p3_underline  <= buf_attr[7];
        p3_slow_blink <= buf_attr[6];
        p3_fast_blink <= buf_attr[5];
        p3_strike     <= buf_attr[4];
        p3_graphic    <= font_graphic;
        p3_char_row   <= y_pos[3:0];

        // Pipeline stage 4: pass through (font output now ready)
        p4_pixel_x    <= p3_pixel_x;
        p4_fg_idx     <= p3_fg_idx;
        p4_bg_idx     <= p3_bg_idx;
        p4_underline  <= p3_underline;
        p4_slow_blink <= p3_slow_blink;
        p4_fast_blink <= p3_fast_blink;
        p4_strike     <= p3_strike;
        p4_graphic    <= p3_graphic;
        p4_char_row   <= p3_char_row;
        p4_de         <= p3_de;
        p4_hsync      <= p3_hsync;
        p4_vsync      <= p3_vsync;
        p4_in_text    <= p3_in_text;

        // Character cell counters (mod-9 pixel, mod-142 column)
        // Text area: 142 cols * 9 pixels = 1278, with 1px border each side (x=1..1278)
        IF (display_enable == 1'b0) {
            pixel_x  <= 4'd0;
            text_col <= 8'd0;
            in_text  <= 1'b0;
        } ELIF (x_pos == 11'd1) {
            pixel_x  <= 4'd0;
            text_col <= 8'd0;
            in_text  <= 1'b1;
        } ELIF (in_text == 1'b1) {
            IF (pixel_x == 4'd8) {
                pixel_x <= 4'd0;
                IF (text_col == 8'd141) {
                    in_text <= 1'b0;
                } ELSE {
                    text_col <= text_col + 8'd1;
                }
            } ELSE {
                pixel_x <= pixel_x + 4'd1;
            }
        }

        // Blink counter (free-running)
        blink_cnt <= blink_cnt + 27'd1;
    }
@endmod
jz
// Font Cache: SDRAM-backed font with double-buffered scanline cache
//
// Font data lives in SDRAM (written by lzss_decomp at startup).
// Two 256x8 BSRAM caches hold font bitmaps for the current and next pixel rows.
//
// Pipeline timing: 4 cycles from text_col to bitmap output, matching font_ram.
//   Cycles 1-2: delay registers (col_d1, col_d2) to match terminal buffer latency
//   Cycle 3: BSRAM cache read (address = col_d2)
//   Cycle 4: bank mux into bitmap_out
//
// Character capture: During display, characters seen at each column are saved
// in a register-file buffer. During prefetch, this buffer is read to compute
// SDRAM addresses for the next pixel row's font bytes.
//
// SDRAM font layout: address = FONT_BASE + glyph_idx * 16 + char_row
//   glyph_idx = {bank[4:0], char_code[6:0]}
//   One byte per 32-bit SDRAM word (low 8 bits).
@module font_cache
    CONST {
        FONT_BASE   = 65536;  // 0x010000 in SDRAM
        MAX_COL     = 141;

        // States
        ST_IDLE     = 0;
        ST_FETCH    = 1;  // Issue SDRAM read for prefetch
        ST_WAIT     = 2;  // Waiting for SDRAM done
    }

    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;

        // Font lookup (same interface as font_ram)
        IN  [16] char_code;
        IN  [4]  row;
        OUT [8]  bitmap;
        OUT [1]  graphic;

        // Prefetch coordination from pixel generator
        IN  [4]  char_row;         // Current pixel row within character cell (y_pos[3:0])
        IN  [8]  text_col;         // Current text column being rendered
        IN  [16] pf_char;          // Character code at current column (from terminal buffer)

        // Ready signal (font data has been decompressed to SDRAM)
        IN  [1]  font_ready;

        // Display active (HIGH during active pixel scan, LOW during blanking)
        IN  [1]  display_active;

        // SDRAM read interface (directly to arbiter port)
        OUT [21] sd_addr;
        OUT [1]  sd_rd;
        IN  [32] sd_rdata;
        IN  [1]  sd_busy;
        IN  [1]  sd_done;
    }

    WIRE {
        // Font lookup (for graphic flag)
        glyph_idx    [12];

        // Prefetch: character from capture buffer
        pf_saved_char [16];

        // Prefetch glyph computation (from saved character)
        pf_glyph_idx [12];
    }

    REGISTER {
        // Pipeline delay to match terminal buffer latency (2 cycles)
        col_d1        [8]  = 8'd0;
        col_d2        [8]  = 8'd0;

        // Cache control
        active_buf    [1]  = 1'b0;    // Which cache pixel gen reads (0=A, 1=B)
        cached_row    [4]  = 4'd15;   // Pixel row in active cache
        prefetch_row  [4]  = 4'd15;   // Pixel row in prefetch cache

        // Bitmap output (registered mux, like font_ram)
        bitmap_out    [8]  = 8'h00;
        graphic_r     [1]  = 1'b0;

        // Prefetch state machine
        state         [2]  = 2'd0;
        pf_col_r      [8]  = 8'd0;   // Current prefetch column
        pf_target_row [4]  = 4'd0;   // Row being prefetched

        // SDRAM request
        sd_addr_r     [21] = 21'd0;
        sd_rd_r       [1]  = 1'b0;

        // Line buffer write staging
        lb_wr_en      [1]  = 1'b0;
        lb_wr_to_a    [1]  = 1'b0;
        lb_wr_addr_r  [8]  = 8'd0;
        lb_wr_data_r  [8]  = 8'h00;

        // Cache read bank selector (registered for pipeline)
        lb_bank_sel   [1]  = 1'b0;
    }

    MEM(TYPE=BLOCK) {
        // Scanline cache A: 8-bit wide x 256 deep (uses 142 entries)
        cache_a [8] [256] = 8'h00 { OUT rd SYNC; IN wr; };

        // Scanline cache B
        cache_b [8] [256] = 8'h00 { OUT rd SYNC; IN wr; };
    }

    MEM(TYPE=DISTRIBUTED) {
        // Character capture buffer: stores char codes seen during display
        // Written during display scan, read during prefetch
        char_buf [16] [256] = 16'h0000 { OUT rd ASYNC; IN wr; };
    }

    ASYNCHRONOUS {
        // Unicode -> glyph index mapping (for graphic flag, same as font_ram.jz)
        IF (char_code[15:8] == 8'h00) {
            glyph_idx <= { 4'b0000, char_code[7:0] };
        } ELIF (char_code[15:7] == 9'b000000010) {
            glyph_idx <= { 5'd2, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b000000011) {
            glyph_idx <= { 5'd3, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b000000100) {
            glyph_idx <= { 5'd4, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b000000110) {
            glyph_idx <= { 5'd5, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b000000111) {
            glyph_idx <= { 5'd6, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b000001000) {
            glyph_idx <= { 5'd7, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b000001001) {
            glyph_idx <= { 5'd8, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b000001010) {
            glyph_idx <= { 5'd9, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b001000000) {
            glyph_idx <= { 5'd10, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b001000001) {
            glyph_idx <= { 5'd11, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b001000010) {
            glyph_idx <= { 5'd12, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b001000011) {
            glyph_idx <= { 5'd13, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b001000100) {
            glyph_idx <= { 5'd14, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b001000101) {
            glyph_idx <= { 5'd15, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b001001010) {
            glyph_idx <= { 5'd16, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b001001011) {
            glyph_idx <= { 5'd17, char_code[6:0] };
        // Latin Extended Additional (U+1E00-1EFF)
        } ELIF (char_code[15:7] == 9'b000111100) {
            glyph_idx <= { 5'd22, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b000111101) {
            glyph_idx <= { 5'd23, char_code[6:0] };
        // Braille (U+2800-28FF)
        } ELIF (char_code[15:7] == 9'b001010000) {
            glyph_idx <= { 5'd24, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b001010001) {
            glyph_idx <= { 5'd25, char_code[6:0] };
        // Wide char right halves (bit 15 set by terminal.jz)
        } ELIF (char_code[15:7] == 9'b101100000) {
            glyph_idx <= { 5'd20, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b101100001) {
            glyph_idx <= { 5'd21, char_code[6:0] };
        // Kanji right halves (U+4000 | 0x8000 = U+C0xx)
        } ELIF (char_code[15:7] == 9'b110000000) {
            glyph_idx <= { 5'd28, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b110000001) {
            glyph_idx <= { 5'd29, char_code[6:0] };
        // Wide char left halves
        } ELIF (char_code[15:7] == 9'b001100000) {
            glyph_idx <= { 5'd18, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b001100001) {
            glyph_idx <= { 5'd19, char_code[6:0] };
        // Kanji left halves (U+4000-40FF)
        } ELIF (char_code[15:7] == 9'b010000000) {
            glyph_idx <= { 5'd26, char_code[6:0] };
        } ELIF (char_code[15:7] == 9'b010000001) {
            glyph_idx <= { 5'd27, char_code[6:0] };
        } ELSE {
            glyph_idx <= 12'd63;
        }

        // Read character capture buffer at prefetch column
        pf_saved_char <= char_buf.rd[pf_col_r];

        // Compute glyph index from saved character (for SDRAM address)
        IF (pf_saved_char[15:8] == 8'h00) {
            pf_glyph_idx <= { 4'b0000, pf_saved_char[7:0] };
        } ELIF (pf_saved_char[15:7] == 9'b000000010) {
            pf_glyph_idx <= { 5'd2, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b000000011) {
            pf_glyph_idx <= { 5'd3, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b000000100) {
            pf_glyph_idx <= { 5'd4, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b000000110) {
            pf_glyph_idx <= { 5'd5, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b000000111) {
            pf_glyph_idx <= { 5'd6, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b000001000) {
            pf_glyph_idx <= { 5'd7, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b000001001) {
            pf_glyph_idx <= { 5'd8, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b000001010) {
            pf_glyph_idx <= { 5'd9, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b001000000) {
            pf_glyph_idx <= { 5'd10, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b001000001) {
            pf_glyph_idx <= { 5'd11, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b001000010) {
            pf_glyph_idx <= { 5'd12, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b001000011) {
            pf_glyph_idx <= { 5'd13, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b001000100) {
            pf_glyph_idx <= { 5'd14, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b001000101) {
            pf_glyph_idx <= { 5'd15, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b001001010) {
            pf_glyph_idx <= { 5'd16, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b001001011) {
            pf_glyph_idx <= { 5'd17, pf_saved_char[6:0] };
        // Latin Extended Additional (U+1E00-1EFF)
        } ELIF (pf_saved_char[15:7] == 9'b000111100) {
            pf_glyph_idx <= { 5'd22, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b000111101) {
            pf_glyph_idx <= { 5'd23, pf_saved_char[6:0] };
        // Braille (U+2800-28FF)
        } ELIF (pf_saved_char[15:7] == 9'b001010000) {
            pf_glyph_idx <= { 5'd24, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b001010001) {
            pf_glyph_idx <= { 5'd25, pf_saved_char[6:0] };
        // Wide char right halves (bit 15 set by terminal.jz)
        } ELIF (pf_saved_char[15:7] == 9'b101100000) {
            pf_glyph_idx <= { 5'd20, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b101100001) {
            pf_glyph_idx <= { 5'd21, pf_saved_char[6:0] };
        // Kanji right halves (U+4000 | 0x8000 = U+C0xx)
        } ELIF (pf_saved_char[15:7] == 9'b110000000) {
            pf_glyph_idx <= { 5'd28, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b110000001) {
            pf_glyph_idx <= { 5'd29, pf_saved_char[6:0] };
        // Wide char left halves
        } ELIF (pf_saved_char[15:7] == 9'b001100000) {
            pf_glyph_idx <= { 5'd18, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b001100001) {
            pf_glyph_idx <= { 5'd19, pf_saved_char[6:0] };
        // Kanji left halves (U+4000-40FF)
        } ELIF (pf_saved_char[15:7] == 9'b010000000) {
            pf_glyph_idx <= { 5'd26, pf_saved_char[6:0] };
        } ELIF (pf_saved_char[15:7] == 9'b010000001) {
            pf_glyph_idx <= { 5'd27, pf_saved_char[6:0] };
        } ELSE {
            pf_glyph_idx <= 12'd63;
        }

        // Output bitmap from cache
        bitmap  <= bitmap_out;
        graphic <= graphic_r;

        // SDRAM interface
        sd_addr <= sd_addr_r;
        sd_rd   <= sd_rd_r & ~sd_done;
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        // === Pipeline delay to match terminal buffer latency ===
        // text_col → col_d1 → col_d2 → cache.rd.addr → bitmap_out
        // This gives 4-cycle latency from text_col, matching:
        //   terminal_buffer (2 cycles) + font_ram (2 cycles)
        col_d1 <= text_col;
        col_d2 <= col_d1;

        // === Cache read path (pixel generator) ===
        // Address cache with delayed column (aligned with buf_char timing)
        cache_a.rd.addr <= col_d2;
        cache_b.rd.addr <= col_d2;
        lb_bank_sel     <= active_buf;

        // Capture cache output (mux based on registered bank select)
        IF (lb_bank_sel == 1'b0) {
            bitmap_out <= cache_a.rd.data;
        } ELSE {
            bitmap_out <= cache_b.rd.data;
        }

        // Graphic flag: characters with glyph_idx >= 256 are graphic (bank >= 2)
        graphic_r <= (glyph_idx >= 12'd256) ? 1'b1 : 1'b0;

        // === Character capture ===
        // Save characters seen during display for prefetch.
        // col_d2 aligns with pf_char (both reflect the same column).
        char_buf.wr[col_d2] <= pf_char;

        // === Single cache write site (from staged values) ===
        IF (lb_wr_en == 1'b1 && lb_wr_to_a == 1'b1) {
            cache_a.wr[lb_wr_addr_r] <= lb_wr_data_r;
        } ELIF (lb_wr_en == 1'b1 && lb_wr_to_a == 1'b0) {
            cache_b.wr[lb_wr_addr_r] <= lb_wr_data_r;
        }

        // === Unified state machine (includes cache swap + prefetch) ===
        SELECT (state) {
            CASE (lit(2, ST_IDLE)) {
                lb_wr_en <= 1'b0;
                sd_rd_r  <= 1'b0;

                // Cache swap: when prefetch buffer has the row we need
                // and active cache doesn't. ONLY swap during horizontal
                // blanking to prevent mid-scanline tearing (prefetch can
                // take longer than one scanline, causing partial updates).
                // When swap is pending but display is active, do nothing
                // (don't start a new prefetch that would overwrite the
                // inactive cache we're about to swap in).
                IF (prefetch_row == char_row && cached_row != char_row) {
                    IF (display_active == 1'b0) {
                        active_buf   <= ~active_buf;
                        cached_row   <= prefetch_row;
                        prefetch_row <= cached_row;
                    }
                } ELIF (font_ready == 1'b1) {
                    // Determine what to prefetch
                    IF (char_row != cached_row && char_row != prefetch_row) {
                        // Urgent: current pixel row not in either cache
                        pf_target_row <= char_row;
                        pf_col_r      <= 8'd0;
                        state         <= lit(2, ST_FETCH);
                    } ELIF (cached_row == char_row) {
                        // Prefetch next row (if not already done)
                        IF (char_row == 4'd15) {
                            IF (prefetch_row != 4'd0) {
                                pf_target_row <= 4'd0;
                                pf_col_r      <= 8'd0;
                                state         <= lit(2, ST_FETCH);
                            }
                        } ELSE {
                            IF (prefetch_row != char_row + 4'd1) {
                                pf_target_row <= char_row + 4'd1;
                                pf_col_r      <= 8'd0;
                                state         <= lit(2, ST_FETCH);
                            }
                        }
                    }
                }
            }

            CASE (lit(2, ST_FETCH)) {
                lb_wr_en <= 1'b0;

                // Issue SDRAM read: FONT_BASE + glyph_idx * 16 + target_row
                // glyph_idx * 16 + row = {glyph_idx, row} (concatenation)
                IF (sd_busy == 1'b0) {
                    sd_addr_r <= lit(21, FONT_BASE) + { 5'b00000, pf_glyph_idx, pf_target_row };
                    sd_rd_r   <= 1'b1;
                    state     <= lit(2, ST_WAIT);
                } ELSE {
                    sd_rd_r <= 1'b0;
                }
            }

            CASE (lit(2, ST_WAIT)) {
                IF (sd_done == 1'b1) {
                    sd_rd_r <= 1'b0;

                    // Write SDRAM read data (low byte) to prefetch cache
                    lb_wr_en     <= 1'b1;
                    lb_wr_to_a   <= active_buf;   // Write to inactive cache
                    lb_wr_addr_r <= pf_col_r;
                    lb_wr_data_r <= sd_rdata[7:0];

                    IF (pf_col_r == lit(8, MAX_COL)) {
                        // Prefetch complete
                        prefetch_row <= pf_target_row;
                        state        <= lit(2, ST_IDLE);
                    } ELSE {
                        pf_col_r <= pf_col_r + 8'd1;
                        state    <= lit(2, ST_FETCH);
                    }
                } ELSE {
                    lb_wr_en <= 1'b0;
                }
            }

            DEFAULT {
                sd_rd_r  <= 1'b0;
                lb_wr_en <= 1'b0;
                state    <= lit(2, ST_IDLE);
            }
        }
    }
@endmod
jz
// LZSS + ZeroGlyph Decompressor: reads compressed font data from banked BSRAM, writes to SDRAM
//
// Two-layer compressed format:
//   Layer 1 (outer): ZeroGlyph encoding
//     0x00 + count N (1-255) = N consecutive all-zero 16-byte glyphs
//     0x00 + 0x00 = escape, next 16 bytes are a literal glyph starting with 0x00
//     Any byte 0x01-0xFF = start of a literal 16-byte glyph (read 15 more bytes)
//   Layer 2 (inner): LZSS with lazy matching
//     Flag byte: 8 bits, MSB first. 1=literal, 0=reference.
//     Literal: 1 raw byte.
//     Reference: 2 bytes {offset[11:4], offset[3:0] | (length-3)[3:0]}
//     Sliding window: 4096 bytes, match length 3-18.
//
// Compressed ROM: 8 banks x 2048 x 8-bit = 16384 bytes (8 BSRAM blocks)
// Window buffer: 2 banks x 2048 x 8-bit = 4096 bytes (2 BSRAM blocks)
//
// Output: font data written to SDRAM starting at FONT_BASE (0x010000).
// Each decompressed byte is stored in the low 8 bits of a 32-bit SDRAM word.
//
// Decompressed: 61440 bytes (30 banks x 128 x 16).
@module lzss_decomp
    CONST {
        FONT_BASE    = 65536;  // 0x010000
        DECOMP_SIZE  = 61440;  // Total decompressed bytes (30 banks x 128 x 16)
        WINDOW_MASK  = 4095;   // 0xFFF

        // States
        ST_IDLE      = 0;
        ST_RD_WAIT   = 1;   // Wait for BSRAM read (1 cycle latency)
        ST_PROCESS   = 2;   // Process read data (literal, flag, ref bytes)
        ST_WIN_WAIT  = 3;   // Wait for window BSRAM read
        ST_WIN_PROC  = 4;   // Process window read data (copy byte)
        ST_WR_SDRAM  = 5;   // Write byte to SDRAM
        ST_DONE      = 6;
        ST_ZG_DECODE = 7;   // ZeroGlyph decode an LZSS-produced byte
        ST_ZG_ZERO_WR = 8;  // ZeroGlyph zero-fill loop (write 0x00 to SDRAM)
    }

    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;
        IN  [1]  start;
        OUT [1]  done;

        // SDRAM write interface (directly to arbiter port)
        OUT [21] sd_addr;
        OUT [32] sd_wdata;
        OUT [1]  sd_wr;
        IN  [1]  sd_busy;
        IN  [1]  sd_done;
    }

    WIRE {
        // ROM address decomposition
        rom_bank      [4];
        rom_bank_addr [11];

        // Window address decomposition
        win_rd_bank  [1];
        win_rd_baddr [11];
        win_wr_bank  [1];
        win_wr_baddr [11];
    }

    REGISTER {
        state       [4] = 4'd0;   // ST_IDLE (expanded to 4 bits for 9 states)
        rom_addr    [14] = 14'd0;  // Read pointer into compressed ROM (14 bits for 8 banks)
        out_addr    [16] = 16'd0;  // Output byte counter
        win_wptr    [12] = 12'd0;  // Window write pointer

        // Flag byte processing
        flag_byte   [8] = 8'd0;
        flag_bit    [3] = 3'd0;   // Current bit within flag byte (0-7)
        need_flag   [1] = 1'b1;   // Next read is a flag byte

        // Reference decoding
        ref_b0      [8] = 8'd0;
        ref_have_b0 [1] = 1'b0;   // Have first reference byte
        ref_length  [5] = 5'd0;   // Remaining bytes to copy
        ref_rptr    [12] = 12'd0; // Window read pointer

        // ROM bank select registered (for mux after read)
        rom_bank_r  [4] = 4'd0;

        // ROM data mux output (registered from SYNC block)
        rom_data_r  [8] = 8'h00;

        // Control
        done_r      [1] = 1'b0;
        sd_wr_r     [1] = 1'b0;
        sd_addr_r   [21] = 21'd0;
        sd_wdata_r  [32] = 32'd0;

        // Window write staging
        win_wr_pending [1]  = 1'b0;
        win_wr_ptr_s   [12] = 12'd0;
        win_wr_data_s  [8]  = 8'h00;

        // After SDRAM write: what to do next
        // 0 = advance flag (via ZG decode), 1 = more copy bytes
        after_wr    [1]  = 1'b0;

        // Extra wait cycle for BSRAM pipeline (2 cycles needed: read + bank mux)
        wait_extra  [1]  = 1'b0;

        // ZeroGlyph decoder state
        // zg_mode: 0 = expect marker-or-literal-start byte
        //          1 = expect count byte (after seeing 0x00 marker)
        //          2 = pass-through remaining glyph bytes (literal glyph)
        zg_mode     [2]  = 2'd0;
        zg_remain   [4]  = 4'd0;   // Bytes remaining in current literal glyph (0-15)
        zg_zero_rem [12] = 12'd0;  // Zero bytes remaining to emit (count * 16, max 4080)

        // Saved LZSS return state for after ZG decode produces an SDRAM write
        zg_lzss_after [1] = 1'b0;  // 0 = advance flag, 1 = more copy
    }

    MEM(TYPE=BLOCK) {
        // Compressed font ROM: 8 banks of 2048 x 8-bit
        rom0 [8] [2048] = @file("../out/font_rom0.mem") { OUT rd SYNC; };
        rom1 [8] [2048] = @file("../out/font_rom1.mem") { OUT rd SYNC; };
        rom2 [8] [2048] = @file("../out/font_rom2.mem") { OUT rd SYNC; };
        rom3 [8] [2048] = @file("../out/font_rom3.mem") { OUT rd SYNC; };
        rom4 [8] [2048] = @file("../out/font_rom4.mem") { OUT rd SYNC; };
        rom5 [8] [2048] = @file("../out/font_rom5.mem") { OUT rd SYNC; };
        rom6 [8] [2048] = @file("../out/font_rom6.mem") { OUT rd SYNC; };
        rom7 [8] [2048] = @file("../out/font_rom7.mem") { OUT rd SYNC; };

        // Sliding window: 2 banks of 2048 x 8-bit
        win0 [8] [2048] = 8'h00 { OUT rd SYNC; IN wr; };
        win1 [8] [2048] = 8'h00 { OUT rd SYNC; IN wr; };
    }

    ASYNCHRONOUS {
        done     <= done_r;
        sd_addr  <= sd_addr_r;
        sd_wdata <= sd_wdata_r;
        sd_wr    <= sd_wr_r & ~sd_done;

        // ROM address decomposition (14-bit addr: 3 bits bank, 11 bits offset)
        rom_bank      <= { 1'b0, rom_addr[13:11] };
        rom_bank_addr <= rom_addr[10:0];

        // Window address decomposition
        win_rd_bank  <= ref_rptr[11];
        win_rd_baddr <= ref_rptr[10:0];
        win_wr_bank  <= win_wr_ptr_s[11];
        win_wr_baddr <= win_wr_ptr_s[10:0];
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        // Drive ROM read address to all banks every cycle
        rom0.rd.addr <= rom_bank_addr;
        rom1.rd.addr <= rom_bank_addr;
        rom2.rd.addr <= rom_bank_addr;
        rom3.rd.addr <= rom_bank_addr;
        rom4.rd.addr <= rom_bank_addr;
        rom5.rd.addr <= rom_bank_addr;
        rom6.rd.addr <= rom_bank_addr;
        rom7.rd.addr <= rom_bank_addr;

        // Register bank select for mux
        rom_bank_r <= rom_bank;

        // ROM data mux (registered, 1 cycle after address)
        SELECT (rom_bank_r) {
            CASE (4'd0) { rom_data_r <= rom0.rd.data; }
            CASE (4'd1) { rom_data_r <= rom1.rd.data; }
            CASE (4'd2) { rom_data_r <= rom2.rd.data; }
            CASE (4'd3) { rom_data_r <= rom3.rd.data; }
            CASE (4'd4) { rom_data_r <= rom4.rd.data; }
            CASE (4'd5) { rom_data_r <= rom5.rd.data; }
            CASE (4'd6) { rom_data_r <= rom6.rd.data; }
            CASE (4'd7) { rom_data_r <= rom7.rd.data; }
            DEFAULT     { rom_data_r <= 8'h00; }
        }

        // Drive window read address to both banks
        win0.rd.addr <= win_rd_baddr;
        win1.rd.addr <= win_rd_baddr;

        // Window write (from staging registers, then clear pending)
        IF (win_wr_pending == 1'b1 && win_wr_bank == 1'b0) {
            win0.wr[win_wr_baddr] <= win_wr_data_s;
        } ELIF (win_wr_pending == 1'b1 && win_wr_bank == 1'b1) {
            win1.wr[win_wr_baddr] <= win_wr_data_s;
        }

        SELECT (state) {
            CASE (lit(4, ST_IDLE)) {
                sd_wr_r        <= 1'b0;
                win_wr_pending <= 1'b0;
                IF (start == 1'b1) {
                    rom_addr    <= 14'd0;
                    out_addr    <= 16'd0;
                    win_wptr    <= 12'd0;
                    flag_bit    <= 3'd0;
                    need_flag   <= 1'b1;
                    ref_have_b0 <= 1'b0;
                    done_r      <= 1'b0;
                    zg_mode     <= 2'd0;
                    zg_remain   <= 4'd0;
                    zg_zero_rem <= 12'd0;
                    // ROM read issued by always-driven address; wait 2 cycles
                    // (1 for BSRAM latency, 1 for bank mux register)
                    state       <= lit(4, ST_RD_WAIT);
                }
            }

            CASE (lit(4, ST_RD_WAIT)) {
                // Wait for BSRAM read latency + bank mux register (2 cycles)
                sd_wr_r        <= 1'b0;
                win_wr_pending <= 1'b0;
                IF (wait_extra == 1'b0) {
                    wait_extra <= 1'b1;
                } ELSE {
                    wait_extra <= 1'b0;
                    state      <= lit(4, ST_PROCESS);
                }
            }

            CASE (lit(4, ST_PROCESS)) {
                sd_wr_r        <= 1'b0;
                win_wr_pending <= 1'b0;

                // Check completion
                IF (out_addr >= lit(16, DECOMP_SIZE)) {
                    done_r <= 1'b1;
                    state  <= lit(4, ST_DONE);
                } ELIF (need_flag == 1'b1) {
                    // This read was a flag byte
                    flag_byte  <= rom_data_r;
                    flag_bit   <= 3'd0;
                    need_flag  <= 1'b0;
                    rom_addr   <= rom_addr + 14'd1;
                    // Wait for next token read
                    state      <= lit(4, ST_RD_WAIT);
                } ELIF (ref_have_b0 == 1'b1) {
                    // Reference byte 1
                    ref_have_b0 <= 1'b0;
                    ref_length  <= { 1'b0, rom_data_r[3:0] } + 5'd3;
                    ref_rptr    <= (win_wptr - ({ ref_b0, rom_data_r[7:4] }) - 12'd1) & lit(12, WINDOW_MASK);
                    rom_addr    <= rom_addr + 14'd1;
                    // Start window read
                    state       <= lit(4, ST_WIN_WAIT);
                } ELIF (flag_byte[7] == 1'b1) {
                    // Literal byte — route through ZeroGlyph decoder
                    win_wr_pending <= 1'b1;
                    win_wr_ptr_s   <= win_wptr;
                    win_wr_data_s  <= rom_data_r;
                    win_wptr       <= (win_wptr + 12'd1) & lit(12, WINDOW_MASK);
                    rom_addr       <= rom_addr + 14'd1;
                    zg_lzss_after  <= 1'b0;
                    state          <= lit(4, ST_ZG_DECODE);
                } ELSE {
                    // Reference byte 0
                    ref_b0      <= rom_data_r;
                    ref_have_b0 <= 1'b1;
                    rom_addr    <= rom_addr + 14'd1;
                    state       <= lit(4, ST_RD_WAIT);
                }
            }

            CASE (lit(4, ST_WIN_WAIT)) {
                // Wait for window BSRAM read latency (2 cycles)
                sd_wr_r        <= 1'b0;
                win_wr_pending <= 1'b0;
                IF (wait_extra == 1'b0) {
                    wait_extra <= 1'b1;
                } ELSE {
                    wait_extra <= 1'b0;
                    state      <= lit(4, ST_WIN_PROC);
                }
            }

            CASE (lit(4, ST_WIN_PROC)) {
                // Read window data (mux based on bank)
                IF (ref_rptr[11] == 1'b0) {
                    win_wr_data_s <= win0.rd.data;
                } ELSE {
                    win_wr_data_s <= win1.rd.data;
                }

                // Write to window
                win_wr_pending <= 1'b1;
                win_wr_ptr_s   <= win_wptr;
                win_wptr       <= (win_wptr + 12'd1) & lit(12, WINDOW_MASK);

                ref_length <= ref_length - 5'd1;
                ref_rptr   <= (ref_rptr + 12'd1) & lit(12, WINDOW_MASK);

                IF (ref_length == 5'd1) {
                    zg_lzss_after <= 1'b0;
                } ELSE {
                    zg_lzss_after <= 1'b1;
                }
                state <= lit(4, ST_ZG_DECODE);
            }

            CASE (lit(4, ST_ZG_DECODE)) {
                // ZeroGlyph decoder: interpret the LZSS-produced byte (win_wr_data_s)
                // The byte has already been written to the LZSS sliding window.
                win_wr_pending <= 1'b0;

                IF (zg_mode == 2'd0) {
                    // Mode 0: expect marker-or-literal-start byte
                    IF (win_wr_data_s == 8'h00) {
                        // 0x00 marker — next LZSS byte is the count
                        sd_wr_r <= 1'b0;
                        zg_mode <= 2'd1;
                        // Return to LZSS to get next byte
                        IF (zg_lzss_after == 1'b1) {
                            state <= lit(4, ST_WIN_WAIT);
                        } ELSE {
                            flag_byte <= { flag_byte[6:0], 1'b0 };
                            IF (flag_bit == 3'd7) {
                                need_flag <= 1'b1;
                                state     <= lit(4, ST_RD_WAIT);
                            } ELSE {
                                flag_bit <= flag_bit + 3'd1;
                                state    <= lit(4, ST_RD_WAIT);
                            }
                        }
                    } ELSE {
                        // Non-zero first byte: literal glyph, write this byte to SDRAM
                        // then pass through remaining 15 bytes (mode 2 counts 14→0)
                        zg_remain  <= 4'd14;
                        zg_mode    <= 2'd2;
                        sd_addr_r  <= lit(21, FONT_BASE) + { 5'b00000, out_addr };
                        sd_wdata_r <= { 24'd0, win_wr_data_s };
                        sd_wr_r    <= 1'b1;
                        after_wr   <= zg_lzss_after;
                        state      <= lit(4, ST_WR_SDRAM);
                    }
                } ELIF (zg_mode == 2'd1) {
                    // Mode 1: count byte (after 0x00 marker)
                    IF (win_wr_data_s == 8'h00) {
                        // Escape: next 16 bytes are a literal glyph starting with 0x00
                        sd_wr_r   <= 1'b0;
                        zg_remain <= 4'd15;
                        zg_mode   <= 2'd2;
                        // Return to LZSS to get first byte of escaped glyph
                        IF (zg_lzss_after == 1'b1) {
                            state <= lit(4, ST_WIN_WAIT);
                        } ELSE {
                            flag_byte <= { flag_byte[6:0], 1'b0 };
                            IF (flag_bit == 3'd7) {
                                need_flag <= 1'b1;
                                state     <= lit(4, ST_RD_WAIT);
                            } ELSE {
                                flag_bit <= flag_bit + 3'd1;
                                state    <= lit(4, ST_RD_WAIT);
                            }
                        }
                    } ELSE {
                        // N zero glyphs: emit N*16 zero bytes to SDRAM
                        zg_zero_rem <= { win_wr_data_s, 4'd0 };  // count * 16
                        zg_mode     <= 2'd0;
                        // Start zero-fill loop (writes 0x00 to SDRAM without LZSS involvement)
                        sd_addr_r  <= lit(21, FONT_BASE) + { 5'b00000, out_addr };
                        sd_wdata_r <= 32'd0;
                        sd_wr_r    <= 1'b1;
                        state      <= lit(4, ST_ZG_ZERO_WR);
                    }
                } ELIF (zg_mode == 2'd2) {
                    // Mode 2: pass-through glyph bytes
                    sd_addr_r  <= lit(21, FONT_BASE) + { 5'b00000, out_addr };
                    sd_wdata_r <= { 24'd0, win_wr_data_s };
                    sd_wr_r    <= 1'b1;
                    after_wr   <= zg_lzss_after;

                    IF (zg_remain == 4'd0) {
                        // Last byte of this glyph — back to mode 0 after SDRAM write
                        zg_mode <= 2'd0;
                    } ELSE {
                        zg_remain <= zg_remain - 4'd1;
                    }
                    state <= lit(4, ST_WR_SDRAM);
                }
            }

            CASE (lit(4, ST_ZG_ZERO_WR)) {
                // Zero-fill loop: write 0x00 bytes to SDRAM
                win_wr_pending <= 1'b0;
                IF (sd_done == 1'b1) {
                    out_addr    <= out_addr + 16'd1;
                    zg_zero_rem <= zg_zero_rem - 12'd1;

                    IF (out_addr + 16'd1 >= lit(16, DECOMP_SIZE)) {
                        sd_wr_r <= 1'b0;
                        done_r  <= 1'b1;
                        state   <= lit(4, ST_DONE);
                    } ELIF (zg_zero_rem == 12'd1) {
                        // Done with zero-fill, return to LZSS for next byte
                        sd_wr_r <= 1'b0;
                        IF (zg_lzss_after == 1'b1) {
                            state <= lit(4, ST_WIN_WAIT);
                        } ELSE {
                            flag_byte <= { flag_byte[6:0], 1'b0 };
                            IF (flag_bit == 3'd7) {
                                need_flag <= 1'b1;
                                state     <= lit(4, ST_RD_WAIT);
                            } ELSE {
                                flag_bit <= flag_bit + 3'd1;
                                state    <= lit(4, ST_RD_WAIT);
                            }
                        }
                    } ELSE {
                        // More zeros to write
                        sd_addr_r  <= lit(21, FONT_BASE) + { 5'b00000, out_addr + 16'd1 };
                        sd_wdata_r <= 32'd0;
                        sd_wr_r    <= 1'b1;
                    }
                }
            }

            CASE (lit(4, ST_WR_SDRAM)) {
                win_wr_pending <= 1'b0;
                // Wait for SDRAM write to complete
                IF (sd_done == 1'b1) {
                    sd_wr_r  <= 1'b0;
                    out_addr <= out_addr + 16'd1;

                    IF (out_addr + 16'd1 >= lit(16, DECOMP_SIZE)) {
                        done_r <= 1'b1;
                        state  <= lit(4, ST_DONE);
                    } ELIF (after_wr == 1'b1) {
                        state <= lit(4, ST_WIN_WAIT);
                    } ELSE {
                        flag_byte <= { flag_byte[6:0], 1'b0 };
                        IF (flag_bit == 3'd7) {
                            need_flag <= 1'b1;
                            state     <= lit(4, ST_RD_WAIT);
                        } ELSE {
                            flag_bit <= flag_bit + 3'd1;
                            state    <= lit(4, ST_RD_WAIT);
                        }
                    }
                }
            }

            CASE (lit(4, ST_DONE)) {
                done_r         <= 1'b1;
                sd_wr_r        <= 1'b0;
                win_wr_pending <= 1'b0;
            }

            DEFAULT {
                sd_wr_r        <= 1'b0;
                win_wr_pending <= 1'b0;
                state          <= lit(4, ST_IDLE);
            }
        }
    }
@endmod
jz
// 3-Port SDRAM Arbiter
// Multiplexes three clients onto one SDRAM controller with fixed priority:
//   Port 0: Terminal buffer (read/write) — medium priority
//   Port 1: Font cache (read only) — lowest priority
//   Port 2: LZSS decompressor (write only, startup) — highest priority
//
// One transaction at a time: grant held until sd_done.
// Instantiates sdram_ctrl internally.
@module sdram_arb
    CONST {
        // Grant encoding
        GRANT_NONE = 0;
        GRANT_P0   = 1;
        GRANT_P1   = 2;
        GRANT_P2   = 3;
    }

    PORT {
        IN  [1] clk;
        IN  [1] rst_n;

        // Port 0: Terminal buffer (read/write)
        IN  [21] p0_addr;
        IN  [32] p0_wdata;
        IN  [1]  p0_rd;
        IN  [1]  p0_wr;
        OUT [32] p0_rdata;
        OUT [1]  p0_busy;
        OUT [1]  p0_done;

        // Port 1: Font cache (read only)
        IN  [21] p1_addr;
        IN  [1]  p1_rd;
        OUT [32] p1_rdata;
        OUT [1]  p1_busy;
        OUT [1]  p1_done;

        // Port 2: Decompressor (write only)
        IN  [21] p2_addr;
        IN  [32] p2_wdata;
        IN  [1]  p2_wr;
        OUT [1]  p2_busy;
        OUT [1]  p2_done;

        // SDRAM physical interface
        OUT   [1]  sdram_cke;
        OUT   [1]  sdram_cs_n;
        OUT   [1]  sdram_ras_n;
        OUT   [1]  sdram_cas_n;
        OUT   [1]  sdram_wen_n;
        OUT   [4]  sdram_dqm;
        OUT   [11] sdram_addr;
        OUT   [2]  sdram_ba;
        INOUT [32] sdram_dq;
    }

    WIRE {
        // SDRAM controller interface
        sd_addr  [21];
        sd_wdata [32];
        sd_rdata [32];
        sd_rd    [1];
        sd_wr    [1];
        sd_busy  [1];
        sd_done  [1];
    }

    REGISTER {
        grant [2] = 2'd0;  // Current grant holder (GRANT_NONE when idle)
    }

    @new sd0 sdram_ctrl {
        IN  [1]  clk       = clk;
        IN  [1]  rst_n     = rst_n;
        IN  [21] addr      = sd_addr;
        IN  [32] wdata     = sd_wdata;
        OUT [32] rdata     = sd_rdata;
        IN  [1]  rd        = sd_rd;
        IN  [1]  wr        = sd_wr;
        OUT [1]  busy      = sd_busy;
        OUT [1]  done      = sd_done;
        OUT [1]  sdram_cke   = sdram_cke;
        OUT [1]  sdram_cs_n  = sdram_cs_n;
        OUT [1]  sdram_ras_n = sdram_ras_n;
        OUT [1]  sdram_cas_n = sdram_cas_n;
        OUT [1]  sdram_wen_n = sdram_wen_n;
        OUT [4]  sdram_dqm   = sdram_dqm;
        OUT [11] sdram_addr  = sdram_addr;
        OUT [2]  sdram_ba    = sdram_ba;
        INOUT [32] sdram_dq  = sdram_dq;
    }

    ASYNCHRONOUS {
        // Mux SDRAM inputs based on current grant
        IF (grant == lit(2, GRANT_P2)) {
            sd_addr  <= p2_addr;
            sd_wdata <= p2_wdata;
            sd_rd    <= 1'b0;
            sd_wr    <= p2_wr & ~sd_done;
        } ELIF (grant == lit(2, GRANT_P0)) {
            sd_addr  <= p0_addr;
            sd_wdata <= p0_wdata;
            sd_rd    <= p0_rd & ~sd_done;
            sd_wr    <= p0_wr & ~sd_done;
        } ELIF (grant == lit(2, GRANT_P1)) {
            sd_addr  <= p1_addr;
            sd_wdata <= 32'd0;
            sd_rd    <= p1_rd & ~sd_done;
            sd_wr    <= 1'b0;
        } ELSE {
            sd_addr  <= 21'd0;
            sd_wdata <= 32'd0;
            sd_rd    <= 1'b0;
            sd_wr    <= 1'b0;
        }

        // Port 0 outputs
        p0_rdata <= sd_rdata;
        p0_done  <= (grant == lit(2, GRANT_P0)) ? sd_done : 1'b0;
        p0_busy  <= sd_busy | (grant != lit(2, GRANT_P0) && grant != lit(2, GRANT_NONE));

        // Port 1 outputs
        p1_rdata <= sd_rdata;
        p1_done  <= (grant == lit(2, GRANT_P1)) ? sd_done : 1'b0;
        p1_busy  <= sd_busy | (grant != lit(2, GRANT_P1) && grant != lit(2, GRANT_NONE));

        // Port 2 outputs
        p2_done  <= (grant == lit(2, GRANT_P2)) ? sd_done : 1'b0;
        p2_busy  <= sd_busy | (grant != lit(2, GRANT_P2) && grant != lit(2, GRANT_NONE));
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        IF (grant == lit(2, GRANT_NONE) || sd_done == 1'b1) {
            // Arbiter is idle or current transaction just completed.
            // Grant to highest-priority requestor.
            // Priority: P2 (decompressor) > P0 (terminal) > P1 (font)
            IF (p2_wr == 1'b1) {
                grant <= lit(2, GRANT_P2);
            } ELIF (p0_rd == 1'b1 || p0_wr == 1'b1) {
                grant <= lit(2, GRANT_P0);
            } ELIF (p1_rd == 1'b1) {
                grant <= lit(2, GRANT_P1);
            } ELSE {
                grant <= lit(2, GRANT_NONE);
            }
        }
    }
@endmod
jz
@module sdram_ctrl
    CONST {
        INIT_COUNT = 14850;       // 200us * 74.25MHz
        REFRESH_INTERVAL = 579;   // ~7.8us * 74.25MHz
        MODE_REG = 544;           // CL=2, burst=1, sequential

        // GW2AR-18 SDRAM geometry: 2M x 32 = 64Mbit
        ROW_BITS  = 11;
        COL_BITS  = 8;
        BANK_BITS = 2;
        DATA_BITS = 32;
        ADDR_BITS = 21;           // ROW_BITS + COL_BITS + BANK_BITS

        // State machine states
        ST_INIT   = 0;
        ST_IPRE   = 1;
        ST_IREF   = 2;
        ST_IMODE  = 3;
        ST_IDLE   = 4;
        ST_ACT_W  = 5;
        ST_ACT    = 6;
        ST_RD     = 7;
        ST_RD_CL  = 8;
        ST_WR     = 9;
        ST_REF    = 10;
    }

    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;

        // User interface
        IN  [21] addr;
        IN  [32] wdata;
        OUT [32] rdata;
        IN  [1]  rd;
        IN  [1]  wr;
        OUT [1]  busy;
        OUT [1]  done;

        // SDRAM physical interface
        OUT [1]  sdram_cke;
        OUT [1]  sdram_cs_n;
        OUT [1]  sdram_ras_n;
        OUT [1]  sdram_cas_n;
        OUT [1]  sdram_wen_n;
        OUT [4]  sdram_dqm;
        OUT [11] sdram_addr;
        OUT [2]  sdram_ba;
        INOUT [32] sdram_dq;
    }

    REGISTER {
        state       [4]  = 4'd0;
        init_cnt    [14] = 14'b0;
        wait_cnt    [3]  = 3'b0;
        ref_cnt     [10] = 10'b0;
        ref_done    [1]  = 1'b0;

        // Command registers
        r_cke       [1]  = 1'b0;
        r_cs_n      [1]  = 1'b1;
        r_ras_n     [1]  = 1'b1;
        r_cas_n     [1]  = 1'b1;
        r_wen_n     [1]  = 1'b1;
        r_addr      [11] = 11'b0;
        r_ba        [2]  = 2'b0;
        r_dqm       [4]  = 4'b1111;

        // DQ control
        r_dq_oe     [1]  = 1'b0;
        r_dq_out    [32] = 32'b0;

        // Latched request
        r_req_addr  [21] = 21'b0;
        r_req_wdata [32] = 32'b0;
        r_req_write [1]  = 1'b0;

        // Output
        r_rdata     [32] = 32'b0;
        r_done      [1]  = 1'b0;
    }

    ASYNCHRONOUS {
        // Drive SDRAM pins from registers
        sdram_cke   <= r_cke;
        sdram_cs_n  <= r_cs_n;
        sdram_ras_n <= r_ras_n;
        sdram_cas_n <= r_cas_n;
        sdram_wen_n <= r_wen_n;
        sdram_dqm   <= r_dqm;
        sdram_addr  <= r_addr;
        sdram_ba    <= r_ba;

        // Tristate DQ bus
        sdram_dq <= (r_dq_oe == 1'b1) ? r_dq_out : 32'bz;

        // User interface outputs
        rdata <= r_rdata;
        done  <= r_done;
        busy  <= (state != lit(4, ST_IDLE));
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        SELECT(state) {
            // ---- INIT: Power-up wait (200us) ----
            CASE (lit(4, ST_INIT)) {
                r_cke   <= 1'b1;
                r_dq_oe <= 1'b0;
                r_done  <= 1'b0;

                IF (init_cnt == lit(14, INIT_COUNT)) {
                    // PRECHARGE ALL: cs=0, ras=0, cas=1, we=0, A10=1
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b0;
                    r_cas_n <= 1'b1;
                    r_wen_n <= 1'b0;
                    r_addr  <= 11'b10000000000;
                    wait_cnt <= 3'd1;
                    state <= lit(4, ST_IPRE);
                } ELSE {
                    // INHIBIT
                    r_cs_n  <= 1'b1;
                    r_ras_n <= 1'b1;
                    r_cas_n <= 1'b1;
                    r_wen_n <= 1'b1;
                    init_cnt <= init_cnt + 14'b1;
                }
            }

            // ---- IPRE: Wait after PRECHARGE ALL ----
            CASE (lit(4, ST_IPRE)) {
                r_done <= 1'b0;
                IF (wait_cnt == 3'b0) {
                    // AUTO REFRESH: cs=0, ras=0, cas=0, we=1
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b0;
                    r_cas_n <= 1'b0;
                    r_wen_n <= 1'b1;
                    wait_cnt <= 3'd2;
                    state <= lit(4, ST_IREF);
                } ELSE {
                    // NOP
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b1;
                    r_cas_n <= 1'b1;
                    r_wen_n <= 1'b1;
                    wait_cnt <= wait_cnt - 3'b1;
                }
            }

            // ---- IREF: Init AUTO REFRESH (done twice) ----
            CASE (lit(4, ST_IREF)) {
                r_done <= 1'b0;
                IF (wait_cnt == 3'b0) {
                    IF (ref_done == 1'b0) {
                        // Second AUTO REFRESH
                        r_cs_n  <= 1'b0;
                        r_ras_n <= 1'b0;
                        r_cas_n <= 1'b0;
                        r_wen_n <= 1'b1;
                        wait_cnt <= 3'd2;
                        ref_done <= 1'b1;
                    } ELSE {
                        // MODE SET: cs=0, ras=0, cas=0, we=0, addr=mode
                        r_cs_n  <= 1'b0;
                        r_ras_n <= 1'b0;
                        r_cas_n <= 1'b0;
                        r_wen_n <= 1'b0;
                        r_addr  <= lit(11, MODE_REG);
                        r_ba    <= 2'b0;
                        wait_cnt <= 3'd2;
                        state <= lit(4, ST_IMODE);
                    }
                } ELSE {
                    // NOP
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b1;
                    r_cas_n <= 1'b1;
                    r_wen_n <= 1'b1;
                    wait_cnt <= wait_cnt - 3'b1;
                }
            }

            // ---- IMODE: Wait after MODE REGISTER SET ----
            CASE (lit(4, ST_IMODE)) {
                r_done <= 1'b0;
                IF (wait_cnt == 3'b0) {
                    // NOP, go to IDLE
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b1;
                    r_cas_n <= 1'b1;
                    r_wen_n <= 1'b1;
                    r_dqm   <= 4'b0000;
                    state <= lit(4, ST_IDLE);
                } ELSE {
                    // NOP
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b1;
                    r_cas_n <= 1'b1;
                    r_wen_n <= 1'b1;
                    wait_cnt <= wait_cnt - 3'b1;
                }
            }

            // ---- IDLE: Ready for commands ----
            // rd/wr checked before refresh to prevent lost pulses.
            // Delaying refresh by one access (~8 cycles) is within SDRAM timing margin.
            CASE (lit(4, ST_IDLE)) {
                r_dq_oe <= 1'b0;
                r_done  <= 1'b0;

                IF ((rd == 1'b1 || wr == 1'b1) && r_done == 1'b0) {
                    // Latch request
                    r_req_addr  <= addr;
                    r_req_wdata <= wdata;
                    r_req_write <= wr;

                    // ACTIVATE: cs=0, ras=0, cas=1, we=1
                    // Bank = addr[20:19], Row = addr[18:8]
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b0;
                    r_cas_n <= 1'b1;
                    r_wen_n <= 1'b1;
                    r_addr  <= addr[18:8];
                    r_ba    <= addr[20:19];
                    ref_cnt <= ref_cnt + 10'b1;
                    state <= lit(4, ST_ACT_W);
                } ELIF (ref_cnt == lit(10, REFRESH_INTERVAL)) {
                    // AUTO REFRESH: cs=0, ras=0, cas=0, we=1
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b0;
                    r_cas_n <= 1'b0;
                    r_wen_n <= 1'b1;
                    ref_cnt  <= 10'b0;
                    wait_cnt <= 3'd2;
                    state <= lit(4, ST_REF);
                } ELSE {
                    ref_cnt <= ref_cnt + 10'b1;

                    // NOP
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b1;
                    r_cas_n <= 1'b1;
                    r_wen_n <= 1'b1;
                }
            }

            // ---- ACT_W: NOP wait for tRCD (20ns needs 2 cycles at 74.25MHz) ----
            CASE (lit(4, ST_ACT_W)) {
                r_done <= 1'b0;
                // NOP while waiting for tRCD
                r_cs_n  <= 1'b0;
                r_ras_n <= 1'b1;
                r_cas_n <= 1'b1;
                r_wen_n <= 1'b1;
                state <= lit(4, ST_ACT);
            }

            // ---- ACT: Issue READ or WRITE command ----
            CASE (lit(4, ST_ACT)) {
                r_done <= 1'b0;

                IF (r_req_write == 1'b1) {
                    // WRITE: cs=0, ras=1, cas=0, we=0, A10=1 (auto-precharge)
                    // Col = addr[7:0]
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b1;
                    r_cas_n <= 1'b0;
                    r_wen_n <= 1'b0;
                    r_addr  <= {1'b1, 2'b0, r_req_addr[7:0]};
                    r_dq_oe <= 1'b1;
                    r_dq_out <= r_req_wdata;
                    r_dqm   <= 4'b0000;
                    state <= lit(4, ST_WR);
                } ELSE {
                    // READ: cs=0, ras=1, cas=0, we=1, A10=1 (auto-precharge)
                    // Col = addr[7:0]
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b1;
                    r_cas_n <= 1'b0;
                    r_wen_n <= 1'b1;
                    r_addr  <= {1'b1, 2'b0, r_req_addr[7:0]};
                    r_dq_oe <= 1'b0;
                    r_dqm   <= 4'b0000;
                    wait_cnt <= 3'd2;
                    state <= lit(4, ST_RD);
                }
            }

            // ---- RD: READ command issued, start CAS latency wait ----
            CASE (lit(4, ST_RD)) {
                r_done <= 1'b0;
                // NOP while waiting
                r_cs_n  <= 1'b0;
                r_ras_n <= 1'b1;
                r_cas_n <= 1'b1;
                r_wen_n <= 1'b1;
                r_dq_oe <= 1'b0;
                wait_cnt <= wait_cnt - 3'b1;
                state <= lit(4, ST_RD_CL);
            }

            // ---- RD_CL: Waiting for CAS latency ----
            CASE (lit(4, ST_RD_CL)) {
                // NOP
                r_cs_n  <= 1'b0;
                r_ras_n <= 1'b1;
                r_cas_n <= 1'b1;
                r_wen_n <= 1'b1;
                r_dq_oe <= 1'b0;

                IF (wait_cnt == 3'b0) {
                    // Data valid, capture it
                    r_rdata <= sdram_dq;
                    r_done  <= 1'b1;
                    state <= lit(4, ST_IDLE);
                } ELSE {
                    r_done <= 1'b0;
                    wait_cnt <= wait_cnt - 3'b1;
                }
            }

            // ---- WR: WRITE command issued ----
            CASE (lit(4, ST_WR)) {
                // NOP, clear DQ drive
                r_cs_n  <= 1'b0;
                r_ras_n <= 1'b1;
                r_cas_n <= 1'b1;
                r_wen_n <= 1'b1;
                r_dq_oe <= 1'b0;
                r_done  <= 1'b1;
                state <= lit(4, ST_IDLE);
            }

            // ---- REF: Periodic auto refresh ----
            CASE (lit(4, ST_REF)) {
                r_done <= 1'b0;
                IF (wait_cnt == 3'b0) {
                    // NOP, return to IDLE
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b1;
                    r_cas_n <= 1'b1;
                    r_wen_n <= 1'b1;
                    state <= lit(4, ST_IDLE);
                } ELSE {
                    // NOP while waiting
                    r_cs_n  <= 1'b0;
                    r_ras_n <= 1'b1;
                    r_cas_n <= 1'b1;
                    r_wen_n <= 1'b1;
                    wait_cnt <= wait_cnt - 3'b1;
                }
            }

            DEFAULT {
                r_done <= 1'b0;
                // NOP
                r_cs_n  <= 1'b0;
                r_ras_n <= 1'b1;
                r_cas_n <= 1'b1;
                r_wen_n <= 1'b1;
                state <= lit(4, ST_IDLE);
            }
        }
    }
@endmod
jz
// EDID Reader — reads 128-byte EDID block from monitor via DDC/I2C
// On reset, performs I2C read from address 0xA0, stores to internal memory,
// parses descriptor blocks for monitor name (FC), serial (FF), text (FE),
// and extracts resolution from preferred timing descriptor.
// Exposes a byte-read interface: (field, byte_idx) → byte_out.
@module edid_reader
    CONST {
        // I2C addresses
        I2C_ADDR_W = 160;    // 0xA0 — EDID write address
        I2C_ADDR_R = 161;    // 0xA1 — EDID read address

        // Timing (74.25 MHz / 100 kHz / 4 ≈ 185)
        QUARTER_BIT = 185;
        INIT_DELAY  = 1113750;  // ~15ms power-up delay at 74.25 MHz

        // I2C bit-level states
        I2C_IDLE    = 0;
        I2C_START   = 1;
        I2C_TXBYTE  = 2;
        I2C_RXBYTE  = 3;
        I2C_STOP    = 4;

        // Sequencer states
        SEQ_INIT      = 0;
        SEQ_START_W   = 1;
        SEQ_ADDR_W    = 2;
        SEQ_BYTE_ADDR = 3;
        SEQ_START_R   = 4;
        SEQ_ADDR_R    = 5;
        SEQ_READ      = 6;
        SEQ_STOP      = 7;
        SEQ_PARSE     = 8;
        SEQ_BCD_H     = 9;
        SEQ_BCD_V     = 10;
        SEQ_DONE      = 11;
        SEQ_ERROR     = 12;

        // Descriptor tags
        TAG_FC = 252;   // 0xFC — Monitor Name
        TAG_FF = 255;   // 0xFF — Serial Number
        TAG_FE = 254;   // 0xFE — Unspecified Text
    }

    PORT {
        IN    [1]  clk;
        IN    [1]  rst_n;
        INOUT [1]  sda;
        OUT   [1]  scl;

        // Byte read interface
        IN  [2]  field;       // 0=name(FC), 1=serial(FF), 2=text(FE), 3=resolution
        IN  [4]  byte_idx;    // 0-12 (13 chars per descriptor)
        OUT [8]  byte_out;

        OUT [1]  ready;       // EDID data valid and parsed
        OUT [1]  error;       // I2C failed
    }

    WIRE {
        byte_addr_for_read [7];
        rd_addr_mux        [7];
        byte_out_w         [8];
        edid_wr_addr       [7];
    }

    REGISTER {
        // I2C bit-level state
        i2c_state  [4]  = 4'd0;
        phase_cnt  [8]  = 8'd0;
        phase      [2]  = 2'd0;
        bit_cnt    [4]  = 4'd0;
        sda_oe     [1]  = 1'b0;
        scl_out    [1]  = 1'b1;
        shift_out  [8]  = 8'h00;
        shift_in   [8]  = 8'h00;
        got_ack    [1]  = 1'b0;
        send_nack  [1]  = 1'b0;

        // SDA synchronizer
        sda_sync1  [1]  = 1'b1;
        sda_sync2  [1]  = 1'b1;

        // Sequencer
        seq_state  [4]  = 4'd0;
        init_cnt   [21] = 21'd0;
        byte_addr  [8]  = 8'd0;

        // EDID status
        edid_ready_r [1] = 1'b0;
        edid_error_r [1] = 1'b0;

        // Parse state
        parse_step  [5]  = 5'd0;
        parse_byte  [8]  = 8'd0;

        // Descriptor data offsets (base+5 for the ASCII data area)
        desc_fc_off [7]  = 7'd0;
        desc_ff_off [7]  = 7'd0;
        desc_fe_off [7]  = 7'd0;
        has_fc      [1]  = 1'b0;
        has_ff      [1]  = 1'b0;
        has_fe      [1]  = 1'b0;

        // Resolution from preferred timing descriptor
        h_pixels    [12] = 12'd0;
        v_pixels    [12] = 12'd0;

        // BCD digits for resolution string
        h_thou [4] = 4'd0;
        h_hund [4] = 4'd0;
        h_tens [4] = 4'd0;
        h_ones [4] = 4'd0;
        v_thou [4] = 4'd0;
        v_hund [4] = 4'd0;
        v_tens [4] = 4'd0;
        v_ones [4] = 4'd0;
        bcd_rem [12] = 12'd0;

        // Byte output register
        byte_out_r [8] = 8'd0;
    }

    MEM(TYPE=DISTRIBUTED) {
        edid [8] [128] = 8'h00 { OUT rd SYNC; IN wr; };
    }

    ASYNCHRONOUS {
        // I2C open-drain SDA: drive low or release
        sda <= (sda_oe == 1'b1) ? 1'b0 : 1'bz;
        scl <= scl_out;

        ready <= edid_ready_r;
        error <= edid_error_r;

        // Read address mux: parse-controlled during parse, terminal-controlled otherwise
        IF (seq_state == lit(4, SEQ_PARSE)) {
            SELECT (parse_step) {
                // Descriptor 0 check (offset 54)
                CASE (5'd0)  { rd_addr_mux <= 7'd54; }
                CASE (5'd1)  { rd_addr_mux <= 7'd55; }
                CASE (5'd2)  { rd_addr_mux <= 7'd57; }
                // Bubble step: sets addr for step 3 (timing path)
                CASE (5'd20) { rd_addr_mux <= 7'd56; }
                // Resolution extraction (from timing desc at 54)
                // Each step N sets addr for step N+1's read
                CASE (5'd3)  { rd_addr_mux <= 7'd58; }
                CASE (5'd4)  { rd_addr_mux <= 7'd59; }
                CASE (5'd5)  { rd_addr_mux <= 7'd61; }
                CASE (5'd6)  { rd_addr_mux <= 7'd61; }
                // Descriptor 1 check (offset 72)
                CASE (5'd7)  { rd_addr_mux <= 7'd72; }
                CASE (5'd8)  { rd_addr_mux <= 7'd73; }
                CASE (5'd9)  { rd_addr_mux <= 7'd75; }
                // Descriptor 2 check (offset 90)
                CASE (5'd10) { rd_addr_mux <= 7'd90; }
                CASE (5'd11) { rd_addr_mux <= 7'd91; }
                CASE (5'd12) { rd_addr_mux <= 7'd93; }
                // Descriptor 3 check (offset 108)
                CASE (5'd13) { rd_addr_mux <= 7'd108; }
                CASE (5'd14) { rd_addr_mux <= 7'd109; }
                CASE (5'd15) { rd_addr_mux <= 7'd111; }
                DEFAULT      { rd_addr_mux <= byte_addr_for_read; }
            }
        } ELSE {
            rd_addr_mux <= byte_addr_for_read;
        }

        // Byte read address from field/byte_idx
        SELECT (field) {
            CASE (2'd0) { byte_addr_for_read <= desc_fc_off + { 3'b000, byte_idx }; }
            CASE (2'd1) { byte_addr_for_read <= desc_ff_off + { 3'b000, byte_idx }; }
            CASE (2'd2) { byte_addr_for_read <= desc_fe_off + { 3'b000, byte_idx }; }
            DEFAULT     { byte_addr_for_read <= 7'd0; }
        }

        // Byte output mux: BCD digits for resolution (field 3),
        // spaces for missing descriptors, memory data otherwise
        IF (field == 2'd3 && edid_ready_r == 1'b1) {
            SELECT (byte_idx) {
                CASE (4'd0) { byte_out_w <= { 4'h3, h_thou }; }
                CASE (4'd1) { byte_out_w <= { 4'h3, h_hund }; }
                CASE (4'd2) { byte_out_w <= { 4'h3, h_tens }; }
                CASE (4'd3) { byte_out_w <= { 4'h3, h_ones }; }
                CASE (4'd4) { byte_out_w <= 8'h78; }
                CASE (4'd5) { byte_out_w <= { 4'h3, v_thou }; }
                CASE (4'd6) { byte_out_w <= { 4'h3, v_hund }; }
                CASE (4'd7) { byte_out_w <= { 4'h3, v_tens }; }
                CASE (4'd8) { byte_out_w <= { 4'h3, v_ones }; }
                DEFAULT     { byte_out_w <= 8'h00; }
            }
        } ELIF (field == 2'd0 && has_fc == 1'b0) {
            byte_out_w <= 8'h20;
        } ELIF (field == 2'd1 && has_ff == 1'b0) {
            byte_out_w <= 8'h20;
        } ELIF (field == 2'd2 && has_fe == 1'b0) {
            byte_out_w <= 8'h20;
        } ELSE {
            byte_out_w <= byte_out_r;
        }

        byte_out <= byte_out_w;

        // EDID write address: byte_addr-1 normally, 127 for the last byte
        IF (byte_addr[7] == 1'b1) {
            edid_wr_addr <= 7'd127;
        } ELSE {
            IF (byte_addr == 8'd0) {
                edid_wr_addr <= 7'd0;
            } ELSE {
                edid_wr_addr <= byte_addr[6:0] - 7'd1;
            }
        }
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        // SDA synchronizer (always runs)
        sda_sync1 <= sda;
        sda_sync2 <= sda_sync1;

        // Memory read port (always driven)
        edid.rd.addr <= rd_addr_mux;
        byte_out_r   <= edid.rd.data;

        // -------------------------------------------------------
        // I2C bit-level state machine
        // -------------------------------------------------------
        IF (i2c_state != lit(4, I2C_IDLE)) {

            IF (phase_cnt != 8'd0) {
                phase_cnt <= phase_cnt - 8'd1;
            } ELSE {
                phase_cnt <= lit(8, QUARTER_BIT);

                SELECT (i2c_state) {
                    // --- START condition ---
                    CASE (lit(4, I2C_START)) {
                        SELECT (phase) {
                            CASE (2'd0) {
                                // Ensure SDA and SCL released (high)
                                sda_oe  <= 1'b0;
                                scl_out <= 1'b1;
                                phase   <= 2'd1;
                            }
                            CASE (2'd1) {
                                // Pull SDA low while SCL high (START)
                                sda_oe <= 1'b1;
                                phase  <= 2'd2;
                            }
                            CASE (2'd2) {
                                // Pull SCL low
                                scl_out   <= 1'b0;
                                i2c_state <= lit(4, I2C_IDLE);
                                phase     <= 2'd0;
                            }
                            DEFAULT {
                                i2c_state <= lit(4, I2C_IDLE);
                                phase     <= 2'd0;
                            }
                        }
                    }

                    // --- Transmit byte (8 data bits + ACK) ---
                    CASE (lit(4, I2C_TXBYTE)) {
                        IF (bit_cnt != 4'd8) {
                            // Data bits (MSB first)
                            SELECT (phase) {
                                CASE (2'd0) {
                                    scl_out <= 1'b0;
                                    // Open-drain: bit=1 → release, bit=0 → drive low
                                    IF (shift_out[7] == 1'b1) {
                                        sda_oe <= 1'b0;
                                    } ELSE {
                                        sda_oe <= 1'b1;
                                    }
                                    phase <= 2'd1;
                                }
                                CASE (2'd1) {
                                    scl_out <= 1'b1;
                                    phase   <= 2'd2;
                                }
                                CASE (2'd2) {
                                    phase <= 2'd3;
                                }
                                CASE (2'd3) {
                                    scl_out   <= 1'b0;
                                    shift_out <= { shift_out[6:0], 1'b0 };
                                    bit_cnt   <= bit_cnt + 4'd1;
                                    phase     <= 2'd0;
                                }
                                DEFAULT {
                                    phase <= 2'd0;
                                }
                            }
                        } ELSE {
                            // ACK phase (bit 8): release SDA, sample for ACK
                            SELECT (phase) {
                                CASE (2'd0) {
                                    scl_out <= 1'b0;
                                    sda_oe  <= 1'b0;
                                    phase   <= 2'd1;
                                }
                                CASE (2'd1) {
                                    scl_out <= 1'b1;
                                    phase   <= 2'd2;
                                }
                                CASE (2'd2) {
                                    // Sample ACK: SDA low = ACK, SDA high = NACK
                                    IF (sda_sync2 == 1'b0) {
                                        got_ack <= 1'b1;
                                    } ELSE {
                                        got_ack <= 1'b0;
                                    }
                                    phase <= 2'd3;
                                }
                                CASE (2'd3) {
                                    scl_out   <= 1'b0;
                                    bit_cnt   <= 4'd0;
                                    i2c_state <= lit(4, I2C_IDLE);
                                    phase     <= 2'd0;
                                }
                                DEFAULT {
                                    phase <= 2'd0;
                                }
                            }
                        }
                    }

                    // --- Receive byte (8 data bits + ACK/NACK) ---
                    CASE (lit(4, I2C_RXBYTE)) {
                        IF (bit_cnt != 4'd8) {
                            // Data bits: release SDA, sample on SCL high
                            SELECT (phase) {
                                CASE (2'd0) {
                                    scl_out <= 1'b0;
                                    sda_oe  <= 1'b0;
                                    phase   <= 2'd1;
                                }
                                CASE (2'd1) {
                                    scl_out <= 1'b1;
                                    phase   <= 2'd2;
                                }
                                CASE (2'd2) {
                                    // Sample SDA, shift into shift_in (MSB first)
                                    shift_in <= { shift_in[6:0], sda_sync2 };
                                    phase    <= 2'd3;
                                }
                                CASE (2'd3) {
                                    scl_out <= 1'b0;
                                    bit_cnt <= bit_cnt + 4'd1;
                                    phase   <= 2'd0;
                                }
                                DEFAULT {
                                    phase <= 2'd0;
                                }
                            }
                        } ELSE {
                            // ACK/NACK phase: drive ACK (low) or NACK (release)
                            SELECT (phase) {
                                CASE (2'd0) {
                                    scl_out <= 1'b0;
                                    // ACK = drive low, NACK = release
                                    IF (send_nack == 1'b1) {
                                        sda_oe <= 1'b0;
                                    } ELSE {
                                        sda_oe <= 1'b1;
                                    }
                                    phase <= 2'd1;
                                }
                                CASE (2'd1) {
                                    scl_out <= 1'b1;
                                    phase   <= 2'd2;
                                }
                                CASE (2'd2) {
                                    phase <= 2'd3;
                                }
                                CASE (2'd3) {
                                    scl_out   <= 1'b0;
                                    sda_oe    <= 1'b0;
                                    bit_cnt   <= 4'd0;
                                    i2c_state <= lit(4, I2C_IDLE);
                                    phase     <= 2'd0;
                                }
                                DEFAULT {
                                    phase <= 2'd0;
                                }
                            }
                        }
                    }

                    // --- STOP condition ---
                    CASE (lit(4, I2C_STOP)) {
                        SELECT (phase) {
                            CASE (2'd0) {
                                // SCL low, SDA driven low
                                scl_out <= 1'b0;
                                sda_oe  <= 1'b1;
                                phase   <= 2'd1;
                            }
                            CASE (2'd1) {
                                // SCL high
                                scl_out <= 1'b1;
                                phase   <= 2'd2;
                            }
                            CASE (2'd2) {
                                // Release SDA (goes high = STOP)
                                sda_oe    <= 1'b0;
                                i2c_state <= lit(4, I2C_IDLE);
                                phase     <= 2'd0;
                            }
                            DEFAULT {
                                i2c_state <= lit(4, I2C_IDLE);
                                phase     <= 2'd0;
                            }
                        }
                    }

                    DEFAULT {
                        i2c_state <= lit(4, I2C_IDLE);
                    }
                }
            }

        // -------------------------------------------------------
        // Sequencer (runs when I2C is idle)
        // -------------------------------------------------------
        } ELSE {

            SELECT (seq_state) {
                CASE (lit(4, SEQ_INIT)) {
                    // Power-up delay
                    IF (init_cnt == lit(21, INIT_DELAY)) {
                        seq_state <= lit(4, SEQ_START_W);
                    } ELSE {
                        init_cnt <= init_cnt + 21'd1;
                    }
                }

                CASE (lit(4, SEQ_START_W)) {
                    // Trigger I2C START
                    i2c_state <= lit(4, I2C_START);
                    phase_cnt <= lit(8, QUARTER_BIT);
                    phase     <= 2'd0;
                    seq_state <= lit(4, SEQ_ADDR_W);
                }

                CASE (lit(4, SEQ_ADDR_W)) {
                    // Send write address 0xA0
                    shift_out <= lit(8, I2C_ADDR_W);
                    bit_cnt   <= 4'd0;
                    i2c_state <= lit(4, I2C_TXBYTE);
                    phase_cnt <= lit(8, QUARTER_BIT);
                    phase     <= 2'd0;
                    seq_state <= lit(4, SEQ_BYTE_ADDR);
                }

                CASE (lit(4, SEQ_BYTE_ADDR)) {
                    // Check ACK from address byte, then send byte address 0x00
                    IF (got_ack == 1'b1) {
                        shift_out <= 8'h00;
                        bit_cnt   <= 4'd0;
                        i2c_state <= lit(4, I2C_TXBYTE);
                        phase_cnt <= lit(8, QUARTER_BIT);
                        phase     <= 2'd0;
                        seq_state <= lit(4, SEQ_START_R);
                    } ELSE {
                        seq_state <= lit(4, SEQ_ERROR);
                    }
                }

                CASE (lit(4, SEQ_START_R)) {
                    // Check ACK, then repeated START
                    IF (got_ack == 1'b1) {
                        i2c_state <= lit(4, I2C_START);
                        phase_cnt <= lit(8, QUARTER_BIT);
                        phase     <= 2'd0;
                        seq_state <= lit(4, SEQ_ADDR_R);
                    } ELSE {
                        seq_state <= lit(4, SEQ_ERROR);
                    }
                }

                CASE (lit(4, SEQ_ADDR_R)) {
                    // Send read address 0xA1
                    shift_out <= lit(8, I2C_ADDR_R);
                    bit_cnt   <= 4'd0;
                    i2c_state <= lit(4, I2C_TXBYTE);
                    phase_cnt <= lit(8, QUARTER_BIT);
                    phase     <= 2'd0;
                    seq_state <= lit(4, SEQ_READ);
                }

                CASE (lit(4, SEQ_READ)) {
                    // byte_addr tracks progress: 0=check ACK, 1-128=store+read
                    // Store received byte (single write point for MEM port)
                    IF (byte_addr != 8'd0) {
                        edid.wr[edid_wr_addr] <= shift_in;
                    }

                    IF (byte_addr == 8'd0) {
                        // First entry: check ACK from address byte
                        IF (got_ack == 1'b1) {
                            // Start receiving first byte
                            send_nack <= 1'b0;
                            bit_cnt   <= 4'd0;
                            i2c_state <= lit(4, I2C_RXBYTE);
                            phase_cnt <= lit(8, QUARTER_BIT);
                            phase     <= 2'd0;
                            byte_addr <= 8'd1;
                        } ELSE {
                            seq_state <= lit(4, SEQ_ERROR);
                        }
                    } ELIF (byte_addr[7] == 1'b1) {
                        // byte_addr >= 128: all 128 bytes received, go to STOP
                        seq_state <= lit(4, SEQ_STOP);
                    } ELSE {
                        // Set up NACK for last byte (byte 127)
                        IF (byte_addr == 8'd127) {
                            send_nack <= 1'b1;
                        } ELSE {
                            send_nack <= 1'b0;
                        }
                        bit_cnt   <= 4'd0;
                        i2c_state <= lit(4, I2C_RXBYTE);
                        phase_cnt <= lit(8, QUARTER_BIT);
                        phase     <= 2'd0;
                        byte_addr <= byte_addr + 8'd1;
                    }
                }

                CASE (lit(4, SEQ_STOP)) {
                    // Trigger I2C STOP
                    i2c_state <= lit(4, I2C_STOP);
                    phase_cnt <= lit(8, QUARTER_BIT);
                    phase     <= 2'd0;
                    parse_step <= 5'd0;
                    seq_state <= lit(4, SEQ_PARSE);
                }

                CASE (lit(4, SEQ_PARSE)) {
                    // Parse EDID descriptors
                    // Each step reads one byte (data available from PREVIOUS step's address)
                    SELECT (parse_step) {
                        CASE (5'd0) {
                            // Set addr=54 (done in ASYNC), advance
                            parse_step <= 5'd1;
                        }
                        CASE (5'd1) {
                            // Got edid[54], store for later. Set addr=55
                            parse_byte <= edid.rd.data;
                            parse_step <= 5'd2;
                        }
                        CASE (5'd2) {
                            // Got edid[55]. Check if timing descriptor (non-zero pixel clock)
                            IF (parse_byte != 8'd0 || edid.rd.data != 8'd0) {
                                // Timing descriptor → extract resolution
                                // Go to bubble step 20 which sets addr=56 for step 3
                                parse_step <= 5'd20;
                            } ELSE {
                                // Display descriptor → check tag at offset 57 (=54+3)
                                // Set addr=57 (done in ASYNC for step 2... wait)
                                // Already set addr=57 for this step. Read will be ready at step 3.
                                // Actually, step 2 sets addr for step 3 read.
                                // For display desc, we want tag at byte 57 (=54+3)
                                // addr=57 is already set by ASYNC for parse_step==2
                                // Data will be available at step 3
                                parse_step <= 5'd16;
                            }
                        }
                        CASE (5'd3) {
                            // Got edid[56] (h_active low byte). Set addr=58
                            parse_byte <= edid.rd.data;
                            parse_step <= 5'd4;
                        }
                        CASE (5'd4) {
                            // Got edid[58] (h_active high nibble in [7:4]). Set addr=59
                            h_pixels   <= { edid.rd.data[7:4], parse_byte };
                            parse_step <= 5'd5;
                        }
                        CASE (5'd5) {
                            // Got edid[59] (v_active low byte). Set addr=61
                            parse_byte <= edid.rd.data;
                            parse_step <= 5'd6;
                        }
                        CASE (5'd6) {
                            // Got edid[61] (v_active high nibble in [7:4])
                            v_pixels   <= { edid.rd.data[7:4], parse_byte };
                            // Now check descriptors 1-3 for tags
                            parse_step <= 5'd7;
                        }

                        // Bubble step: addr=56 set in ASYNC, just advance
                        CASE (5'd20) {
                            parse_step <= 5'd3;
                        }

                        // Descriptor 1 check (offset 72)
                        CASE (5'd7) {
                            // Set addr=72 (done in ASYNC)
                            parse_step <= 5'd8;
                        }
                        CASE (5'd8) {
                            // Got edid[72]. Set addr=73
                            parse_byte <= edid.rd.data;
                            parse_step <= 5'd9;
                        }
                        CASE (5'd9) {
                            // Got edid[73]. Check if display descriptor. Set addr=75
                            IF (parse_byte == 8'd0 && edid.rd.data == 8'd0) {
                                parse_step <= 5'd17;
                            } ELSE {
                                // Not a display descriptor, skip to desc 2
                                parse_step <= 5'd10;
                            }
                        }

                        // Descriptor 2 check (offset 90)
                        CASE (5'd10) {
                            // Set addr=90 (done in ASYNC)
                            parse_step <= 5'd11;
                        }
                        CASE (5'd11) {
                            // Got edid[90]. Set addr=91
                            parse_byte <= edid.rd.data;
                            parse_step <= 5'd12;
                        }
                        CASE (5'd12) {
                            // Got edid[91]. Check if display descriptor. Set addr=93
                            IF (parse_byte == 8'd0 && edid.rd.data == 8'd0) {
                                parse_step <= 5'd18;
                            } ELSE {
                                parse_step <= 5'd13;
                            }
                        }

                        // Descriptor 3 check (offset 108)
                        CASE (5'd13) {
                            // Set addr=108 (done in ASYNC)
                            parse_step <= 5'd14;
                        }
                        CASE (5'd14) {
                            // Got edid[108]. Set addr=109
                            parse_byte <= edid.rd.data;
                            parse_step <= 5'd15;
                        }
                        CASE (5'd15) {
                            // Got edid[109]. Check if display descriptor. Set addr=111
                            IF (parse_byte == 8'd0 && edid.rd.data == 8'd0) {
                                parse_step <= 5'd19;
                            } ELSE {
                                // No more descriptors to check, go to BCD
                                seq_state  <= lit(4, SEQ_BCD_H);
                                parse_step <= 5'd0;
                            }
                        }

                        // Tag reads — descriptor 0 (offset 54), tag at 57
                        CASE (5'd16) {
                            // Got edid[57] — tag byte for desc 0
                            IF (edid.rd.data == lit(8, TAG_FC)) {
                                desc_fc_off <= 7'd59;
                                has_fc      <= 1'b1;
                            } ELIF (edid.rd.data == lit(8, TAG_FF)) {
                                desc_ff_off <= 7'd59;
                                has_ff      <= 1'b1;
                            } ELIF (edid.rd.data == lit(8, TAG_FE)) {
                                desc_fe_off <= 7'd59;
                                has_fe      <= 1'b1;
                            }
                            // Check desc 1
                            parse_step <= 5'd7;
                        }

                        // Tag reads — descriptor 1 (offset 72), tag at 75
                        CASE (5'd17) {
                            // Got edid[75] — tag byte for desc 1
                            IF (edid.rd.data == lit(8, TAG_FC)) {
                                desc_fc_off <= 7'd77;
                                has_fc      <= 1'b1;
                            } ELIF (edid.rd.data == lit(8, TAG_FF)) {
                                desc_ff_off <= 7'd77;
                                has_ff      <= 1'b1;
                            } ELIF (edid.rd.data == lit(8, TAG_FE)) {
                                desc_fe_off <= 7'd77;
                                has_fe      <= 1'b1;
                            }
                            // Check desc 2
                            parse_step <= 5'd10;
                        }

                        // Tag reads — descriptor 2 (offset 90), tag at 93
                        CASE (5'd18) {
                            // Got edid[93] — tag byte for desc 2
                            IF (edid.rd.data == lit(8, TAG_FC)) {
                                desc_fc_off <= 7'd95;
                                has_fc      <= 1'b1;
                            } ELIF (edid.rd.data == lit(8, TAG_FF)) {
                                desc_ff_off <= 7'd95;
                                has_ff      <= 1'b1;
                            } ELIF (edid.rd.data == lit(8, TAG_FE)) {
                                desc_fe_off <= 7'd95;
                                has_fe      <= 1'b1;
                            }
                            // Check desc 3
                            parse_step <= 5'd13;
                        }

                        // Tag reads — descriptor 3 (offset 108), tag at 111
                        CASE (5'd19) {
                            // Got edid[111] — tag byte for desc 3
                            IF (edid.rd.data == lit(8, TAG_FC)) {
                                desc_fc_off <= 7'd113;
                                has_fc      <= 1'b1;
                            } ELIF (edid.rd.data == lit(8, TAG_FF)) {
                                desc_ff_off <= 7'd113;
                                has_ff      <= 1'b1;
                            } ELIF (edid.rd.data == lit(8, TAG_FE)) {
                                desc_fe_off <= 7'd113;
                                has_fe      <= 1'b1;
                            }
                            // Done parsing, go to BCD
                            seq_state  <= lit(4, SEQ_BCD_H);
                            parse_step <= 5'd0;
                        }

                        DEFAULT {
                            seq_state  <= lit(4, SEQ_BCD_H);
                            parse_step <= 5'd0;
                        }
                    }
                }

                CASE (lit(4, SEQ_BCD_H)) {
                    // BCD conversion for horizontal resolution
                    SELECT (parse_step) {
                        CASE (5'd0) {
                            // Extract thousands digit
                            IF (h_pixels >= 12'd4000) {
                                h_thou  <= 4'd4;
                                bcd_rem <= h_pixels - 12'd4000;
                            } ELIF (h_pixels >= 12'd3000) {
                                h_thou  <= 4'd3;
                                bcd_rem <= h_pixels - 12'd3000;
                            } ELIF (h_pixels >= 12'd2000) {
                                h_thou  <= 4'd2;
                                bcd_rem <= h_pixels - 12'd2000;
                            } ELIF (h_pixels >= 12'd1000) {
                                h_thou  <= 4'd1;
                                bcd_rem <= h_pixels - 12'd1000;
                            } ELSE {
                                h_thou  <= 4'd0;
                                bcd_rem <= h_pixels;
                            }
                            parse_step <= 5'd1;
                        }
                        CASE (5'd1) {
                            // Extract hundreds digit
                            IF (bcd_rem >= 12'd900) {
                                h_hund  <= 4'd9;
                                bcd_rem <= bcd_rem - 12'd900;
                            } ELIF (bcd_rem >= 12'd800) {
                                h_hund  <= 4'd8;
                                bcd_rem <= bcd_rem - 12'd800;
                            } ELIF (bcd_rem >= 12'd700) {
                                h_hund  <= 4'd7;
                                bcd_rem <= bcd_rem - 12'd700;
                            } ELIF (bcd_rem >= 12'd600) {
                                h_hund  <= 4'd6;
                                bcd_rem <= bcd_rem - 12'd600;
                            } ELIF (bcd_rem >= 12'd500) {
                                h_hund  <= 4'd5;
                                bcd_rem <= bcd_rem - 12'd500;
                            } ELIF (bcd_rem >= 12'd400) {
                                h_hund  <= 4'd4;
                                bcd_rem <= bcd_rem - 12'd400;
                            } ELIF (bcd_rem >= 12'd300) {
                                h_hund  <= 4'd3;
                                bcd_rem <= bcd_rem - 12'd300;
                            } ELIF (bcd_rem >= 12'd200) {
                                h_hund  <= 4'd2;
                                bcd_rem <= bcd_rem - 12'd200;
                            } ELIF (bcd_rem >= 12'd100) {
                                h_hund  <= 4'd1;
                                bcd_rem <= bcd_rem - 12'd100;
                            } ELSE {
                                h_hund  <= 4'd0;
                            }
                            parse_step <= 5'd2;
                        }
                        CASE (5'd2) {
                            // Extract tens digit
                            IF (bcd_rem >= 12'd90) {
                                h_tens  <= 4'd9;
                                bcd_rem <= bcd_rem - 12'd90;
                            } ELIF (bcd_rem >= 12'd80) {
                                h_tens  <= 4'd8;
                                bcd_rem <= bcd_rem - 12'd80;
                            } ELIF (bcd_rem >= 12'd70) {
                                h_tens  <= 4'd7;
                                bcd_rem <= bcd_rem - 12'd70;
                            } ELIF (bcd_rem >= 12'd60) {
                                h_tens  <= 4'd6;
                                bcd_rem <= bcd_rem - 12'd60;
                            } ELIF (bcd_rem >= 12'd50) {
                                h_tens  <= 4'd5;
                                bcd_rem <= bcd_rem - 12'd50;
                            } ELIF (bcd_rem >= 12'd40) {
                                h_tens  <= 4'd4;
                                bcd_rem <= bcd_rem - 12'd40;
                            } ELIF (bcd_rem >= 12'd30) {
                                h_tens  <= 4'd3;
                                bcd_rem <= bcd_rem - 12'd30;
                            } ELIF (bcd_rem >= 12'd20) {
                                h_tens  <= 4'd2;
                                bcd_rem <= bcd_rem - 12'd20;
                            } ELIF (bcd_rem >= 12'd10) {
                                h_tens  <= 4'd1;
                                bcd_rem <= bcd_rem - 12'd10;
                            } ELSE {
                                h_tens <= 4'd0;
                            }
                            parse_step <= 5'd3;
                        }
                        CASE (5'd3) {
                            h_ones     <= bcd_rem[3:0];
                            parse_step <= 5'd0;
                            seq_state  <= lit(4, SEQ_BCD_V);
                        }
                        DEFAULT {
                            parse_step <= 5'd0;
                            seq_state  <= lit(4, SEQ_BCD_V);
                        }
                    }
                }

                CASE (lit(4, SEQ_BCD_V)) {
                    // BCD conversion for vertical resolution
                    SELECT (parse_step) {
                        CASE (5'd0) {
                            IF (v_pixels >= 12'd4000) {
                                v_thou  <= 4'd4;
                                bcd_rem <= v_pixels - 12'd4000;
                            } ELIF (v_pixels >= 12'd3000) {
                                v_thou  <= 4'd3;
                                bcd_rem <= v_pixels - 12'd3000;
                            } ELIF (v_pixels >= 12'd2000) {
                                v_thou  <= 4'd2;
                                bcd_rem <= v_pixels - 12'd2000;
                            } ELIF (v_pixels >= 12'd1000) {
                                v_thou  <= 4'd1;
                                bcd_rem <= v_pixels - 12'd1000;
                            } ELSE {
                                v_thou  <= 4'd0;
                                bcd_rem <= v_pixels;
                            }
                            parse_step <= 5'd1;
                        }
                        CASE (5'd1) {
                            IF (bcd_rem >= 12'd900) {
                                v_hund  <= 4'd9;
                                bcd_rem <= bcd_rem - 12'd900;
                            } ELIF (bcd_rem >= 12'd800) {
                                v_hund  <= 4'd8;
                                bcd_rem <= bcd_rem - 12'd800;
                            } ELIF (bcd_rem >= 12'd700) {
                                v_hund  <= 4'd7;
                                bcd_rem <= bcd_rem - 12'd700;
                            } ELIF (bcd_rem >= 12'd600) {
                                v_hund  <= 4'd6;
                                bcd_rem <= bcd_rem - 12'd600;
                            } ELIF (bcd_rem >= 12'd500) {
                                v_hund  <= 4'd5;
                                bcd_rem <= bcd_rem - 12'd500;
                            } ELIF (bcd_rem >= 12'd400) {
                                v_hund  <= 4'd4;
                                bcd_rem <= bcd_rem - 12'd400;
                            } ELIF (bcd_rem >= 12'd300) {
                                v_hund  <= 4'd3;
                                bcd_rem <= bcd_rem - 12'd300;
                            } ELIF (bcd_rem >= 12'd200) {
                                v_hund  <= 4'd2;
                                bcd_rem <= bcd_rem - 12'd200;
                            } ELIF (bcd_rem >= 12'd100) {
                                v_hund  <= 4'd1;
                                bcd_rem <= bcd_rem - 12'd100;
                            } ELSE {
                                v_hund  <= 4'd0;
                            }
                            parse_step <= 5'd2;
                        }
                        CASE (5'd2) {
                            IF (bcd_rem >= 12'd90) {
                                v_tens  <= 4'd9;
                                bcd_rem <= bcd_rem - 12'd90;
                            } ELIF (bcd_rem >= 12'd80) {
                                v_tens  <= 4'd8;
                                bcd_rem <= bcd_rem - 12'd80;
                            } ELIF (bcd_rem >= 12'd70) {
                                v_tens  <= 4'd7;
                                bcd_rem <= bcd_rem - 12'd70;
                            } ELIF (bcd_rem >= 12'd60) {
                                v_tens  <= 4'd6;
                                bcd_rem <= bcd_rem - 12'd60;
                            } ELIF (bcd_rem >= 12'd50) {
                                v_tens  <= 4'd5;
                                bcd_rem <= bcd_rem - 12'd50;
                            } ELIF (bcd_rem >= 12'd40) {
                                v_tens  <= 4'd4;
                                bcd_rem <= bcd_rem - 12'd40;
                            } ELIF (bcd_rem >= 12'd30) {
                                v_tens  <= 4'd3;
                                bcd_rem <= bcd_rem - 12'd30;
                            } ELIF (bcd_rem >= 12'd20) {
                                v_tens  <= 4'd2;
                                bcd_rem <= bcd_rem - 12'd20;
                            } ELIF (bcd_rem >= 12'd10) {
                                v_tens  <= 4'd1;
                                bcd_rem <= bcd_rem - 12'd10;
                            } ELSE {
                                v_tens <= 4'd0;
                            }
                            parse_step <= 5'd3;
                        }
                        CASE (5'd3) {
                            v_ones       <= bcd_rem[3:0];
                            edid_ready_r <= 1'b1;
                            seq_state    <= lit(4, SEQ_DONE);
                        }
                        DEFAULT {
                            edid_ready_r <= 1'b1;
                            seq_state    <= lit(4, SEQ_DONE);
                        }
                    }
                }

                CASE (lit(4, SEQ_DONE)) {
                    // Stay here, EDID data available
                }

                CASE (lit(4, SEQ_ERROR)) {
                    edid_error_r <= 1'b1;
                }

                DEFAULT {
                    seq_state <= lit(4, SEQ_ERROR);
                }
            }
        }
    }
@endmod
jz
// 1280x720 @ 60Hz Video Timing Generator
// CEA-861: pixel clock = 74.25 MHz
// H total: 1650 (1280 active + 110 front + 40 sync + 220 back)
// V total: 750  (720 active + 5 front + 5 sync + 20 back)
// Sync polarity: positive (sync HIGH during sync pulse)
@module video_timing
    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;
        OUT [1]  hsync;
        OUT [1]  vsync;
        OUT [1]  display_enable;
        OUT [11] x_pos;
        OUT [10] y_pos;
    }

    CONST {
        // Horizontal timing
        H_ACTIVE = 1280;
        H_FRONT  = 110;
        H_SYNC   = 40;
        H_BACK   = 220;
        H_TOTAL  = 1650;

        // Vertical timing
        V_ACTIVE = 720;
        V_FRONT  = 5;
        V_SYNC   = 5;
        V_BACK   = 20;
        V_TOTAL  = 750;
    }

    REGISTER {
        h_cnt [11] = 11'b0;
        v_cnt [10] = 10'b0;
    }

    ASYNCHRONOUS {
        // Positive sync polarity: HIGH during sync pulse, LOW otherwise
        hsync <= (h_cnt >= lit(11, H_ACTIVE + H_FRONT) &&
                  h_cnt <  lit(11, H_ACTIVE + H_FRONT + H_SYNC))
                 ? 1'b1 : 1'b0;

        vsync <= (v_cnt >= lit(10, V_ACTIVE + V_FRONT) &&
                  v_cnt <  lit(10, V_ACTIVE + V_FRONT + V_SYNC))
                 ? 1'b1 : 1'b0;

        // Display enable: active region
        display_enable <= (h_cnt < lit(11, H_ACTIVE) &&
                           v_cnt < lit(10, V_ACTIVE))
                          ? 1'b1 : 1'b0;

        x_pos <= h_cnt;
        y_pos <= v_cnt;
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        IF (h_cnt == lit(11, H_TOTAL - 1)) {
            h_cnt <= 11'b0;
            IF (v_cnt == lit(10, V_TOTAL - 1)) {
                v_cnt <= 10'b0;
            } ELSE {
                v_cnt <= v_cnt + 10'b1;
            }
        } ELSE {
            h_cnt <= h_cnt + 11'b1;
        }
    }
@endmod
jz
// DVI TMDS 8b/10b Encoder
// Full DVI-compliant TMDS encoding with XOR/XNOR selection and
// running disparity tracking for DC balance on AC-coupled links.
@module tmds_encoder
    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;
        IN  [8]  data_in;
        IN  [1]  c0;
        IN  [1]  c1;
        IN  [1]  display_enable;
        OUT [10] tmds_out;
    }

    WIRE {
        // Popcount of data_in (adder tree)
        d_p0 [2]; d_p1 [2]; d_p2 [2]; d_p3 [2];
        d_s0 [3]; d_s1 [3];
        n1_d [4];

        // XOR/XNOR mode selection
        use_xnor [1];

        // Transition-minimized intermediate word q_m[8:0]
        qm0 [1]; qm1 [1]; qm2 [1]; qm3 [1];
        qm4 [1]; qm5 [1]; qm6 [1]; qm7 [1];
        qm8 [1];

        // Popcount of q_m[7:0] (adder tree)
        q_p0 [2]; q_p1 [2]; q_p2 [2]; q_p3 [2];
        q_s0 [3]; q_s1 [3];
        n1_q [4];

        // Disparity conditions
        cnt_is_zero [1];
        qm_balanced [1];
        cond1       [1];
        cnt_sign    [1];
        cond_inv    [1];

        // Arithmetic for disparity update (5-bit two's complement)
        diff_n1n0 [5];
        diff_n0n1 [5];
        qm8_x2    [5];
        nqm8_x2   [5];

        // Combinational outputs
        tmds_data [10];
        next_cnt  [5];
    }

    REGISTER {
        cnt      [5]  = 5'b00000;
        tmds_reg [10] = 10'b0000000000;
    }

    ASYNCHRONOUS {
        tmds_out <= tmds_reg;

        // --- Popcount of data_in ---
        d_p0 <= {1'b0, data_in[0]} + {1'b0, data_in[1]};
        d_p1 <= {1'b0, data_in[2]} + {1'b0, data_in[3]};
        d_p2 <= {1'b0, data_in[4]} + {1'b0, data_in[5]};
        d_p3 <= {1'b0, data_in[6]} + {1'b0, data_in[7]};
        d_s0 <= {1'b0, d_p0} + {1'b0, d_p1};
        d_s1 <= {1'b0, d_p2} + {1'b0, d_p3};
        n1_d <= {1'b0, d_s0} + {1'b0, d_s1};

        // --- XOR/XNOR selection (DVI spec section 3.3.1) ---
        use_xnor <= (n1_d > 4'd4 || (n1_d == 4'd4 && data_in[0] == 1'b0))
                     ? 1'b1 : 1'b0;

        // --- Build transition-minimized word q_m ---
        qm0 <= data_in[0];
        qm1 <= (use_xnor == 1'b1) ? ~(data_in[1] ^ qm0) : (data_in[1] ^ qm0);
        qm2 <= (use_xnor == 1'b1) ? ~(data_in[2] ^ qm1) : (data_in[2] ^ qm1);
        qm3 <= (use_xnor == 1'b1) ? ~(data_in[3] ^ qm2) : (data_in[3] ^ qm2);
        qm4 <= (use_xnor == 1'b1) ? ~(data_in[4] ^ qm3) : (data_in[4] ^ qm3);
        qm5 <= (use_xnor == 1'b1) ? ~(data_in[5] ^ qm4) : (data_in[5] ^ qm4);
        qm6 <= (use_xnor == 1'b1) ? ~(data_in[6] ^ qm5) : (data_in[6] ^ qm5);
        qm7 <= (use_xnor == 1'b1) ? ~(data_in[7] ^ qm6) : (data_in[7] ^ qm6);
        qm8 <= (use_xnor == 1'b1) ? 1'b0 : 1'b1;

        // --- Popcount of q_m[7:0] ---
        q_p0 <= {1'b0, qm0} + {1'b0, qm1};
        q_p1 <= {1'b0, qm2} + {1'b0, qm3};
        q_p2 <= {1'b0, qm4} + {1'b0, qm5};
        q_p3 <= {1'b0, qm6} + {1'b0, qm7};
        q_s0 <= {1'b0, q_p0} + {1'b0, q_p1};
        q_s1 <= {1'b0, q_p2} + {1'b0, q_p3};
        n1_q <= {1'b0, q_s0} + {1'b0, q_s1};

        // --- Disparity conditions ---
        cnt_is_zero <= (cnt == 5'b00000) ? 1'b1 : 1'b0;
        qm_balanced <= (n1_q == 4'd4) ? 1'b1 : 1'b0;
        cond1       <= (cnt_is_zero == 1'b1 || qm_balanced == 1'b1)
                        ? 1'b1 : 1'b0;
        cnt_sign    <= cnt[4];
        cond_inv    <= ((cnt_sign == 1'b0 && cnt_is_zero == 1'b0 && n1_q > 4'd4) ||
                        (cnt_sign == 1'b1 && n1_q < 4'd4))
                       ? 1'b1 : 1'b0;

        // --- Arithmetic helpers (5-bit two's complement) ---
        diff_n1n0 <= {n1_q, 1'b0} - 5'd8;
        diff_n0n1 <= 5'd8 - {n1_q, 1'b0};
        qm8_x2   <= {3'b000, qm8, 1'b0};
        nqm8_x2  <= {3'b000, ~qm8, 1'b0};

        // --- Output word and next disparity (DVI spec section 3.3.2) ---
        IF (cond1 == 1'b1) {
            IF (qm8 == 1'b0) {
                // XNOR mode, cnt==0 or balanced: invert data, bit[9]=1
                tmds_data <= {1'b1, 1'b0, ~qm7, ~qm6, ~qm5, ~qm4,
                              ~qm3, ~qm2, ~qm1, ~qm0};
                next_cnt  <= cnt + diff_n0n1;
            } ELSE {
                // XOR mode, cnt==0 or balanced: keep data, bit[9]=0
                tmds_data <= {1'b0, 1'b1, qm7, qm6, qm5, qm4,
                              qm3, qm2, qm1, qm0};
                next_cnt  <= cnt + diff_n1n0;
            }
        } ELIF (cond_inv == 1'b1) {
            // Invert to reduce disparity
            tmds_data <= {1'b1, qm8, ~qm7, ~qm6, ~qm5, ~qm4,
                          ~qm3, ~qm2, ~qm1, ~qm0};
            next_cnt  <= cnt + qm8_x2 + diff_n0n1;
        } ELSE {
            // Don't invert
            tmds_data <= {1'b0, qm8, qm7, qm6, qm5, qm4,
                          qm3, qm2, qm1, qm0};
            next_cnt  <= cnt - nqm8_x2 + diff_n1n0;
        }
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        IF (display_enable == 1'b0) {
            // Control period: reset disparity and emit control tokens
            cnt <= 5'b00000;
            IF (c0 == 1'b0 && c1 == 1'b0) {
                tmds_reg <= 10'b1101010100;
            } ELIF (c0 == 1'b1 && c1 == 1'b0) {
                tmds_reg <= 10'b0010101011;
            } ELIF (c0 == 1'b0 && c1 == 1'b1) {
                tmds_reg <= 10'b0101010100;
            } ELSE {
                tmds_reg <= 10'b1010101011;
            }
        } ELSE {
            // Data period: latch encoded word and update disparity
            tmds_reg <= tmds_data;
            cnt <= next_cnt;
        }
    }
@endmod
jz
// Simple UART Receiver — 8N1, no FIFO
// Pulses valid for 1 cycle when a byte is received
@module uart_rx
    CONST {
        // 74.25 MHz / 115200 baud - 1 = 643
        BAUD_DIV = 643;
        HALF_BAUD = 321;
    }

    PORT {
        IN  [1] clk;
        IN  [1] rst_n;
        IN  [1] rx;
        OUT [8] data;
        OUT [1] valid;
    }

    REGISTER {
        // Metastability synchronizer
        rx_sync1    [1] = 1'b1;
        rx_sync2    [1] = 1'b1;

        // State machine (0=IDLE, 1=START, 2=DATA, 3=STOP)
        state       [2] = 2'd0;
        baud_cnt    [10] = 10'd0;
        bit_cnt     [3] = 3'd0;
        shift       [8] = 8'h00;

        // Output
        data_out    [8] = 8'h00;
        valid_out   [1] = 1'b0;
    }

    ASYNCHRONOUS {
        data  <= data_out;
        valid <= valid_out;
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        // 2-stage synchronizer for async RX input
        rx_sync1 <= rx;
        rx_sync2 <= rx_sync1;

        SELECT (state) {
            CASE (2'd0) {
                // IDLE: wait for start bit (falling edge)
                valid_out <= 1'b0;
                IF (rx_sync2 == 1'b0) {
                    baud_cnt <= lit(10, HALF_BAUD);
                    state <= 2'd1;
                }
            }

            CASE (2'd1) {
                // START: verify start bit at mid-point
                valid_out <= 1'b0;
                IF (baud_cnt == 10'd0) {
                    IF (rx_sync2 == 1'b0) {
                        baud_cnt <= lit(10, BAUD_DIV);
                        bit_cnt <= 3'd0;
                        shift <= 8'h00;
                        state <= 2'd2;
                    } ELSE {
                        // False start
                        state <= 2'd0;
                    }
                } ELSE {
                    baud_cnt <= baud_cnt - 10'd1;
                }
            }

            CASE (2'd2) {
                // DATA: sample 8 bits at mid-bit
                valid_out <= 1'b0;
                IF (baud_cnt == 10'd0) {
                    shift <= { rx_sync2, shift[7:1] };
                    IF (bit_cnt == 3'd7) {
                        baud_cnt <= lit(10, BAUD_DIV);
                        state <= 2'd3;
                    } ELSE {
                        bit_cnt <= bit_cnt + 3'd1;
                        baud_cnt <= lit(10, BAUD_DIV);
                    }
                } ELSE {
                    baud_cnt <= baud_cnt - 10'd1;
                }
            }

            CASE (2'd3) {
                // STOP: wait for stop bit, output byte
                IF (baud_cnt == 10'd0) {
                    data_out <= shift;
                    valid_out <= 1'b1;
                    state <= 2'd0;
                } ELSE {
                    valid_out <= 1'b0;
                    baud_cnt <= baud_cnt - 10'd1;
                }
            }

            DEFAULT {
                valid_out <= 1'b0;
                state <= 2'd0;
            }
        }
    }
@endmod
jz
// Simple UART Transmitter — 8N1, no FIFO
// Asserts ready when idle. When valid is pulsed with data, transmits one byte.
@module uart_tx
    CONST {
        // 74.25 MHz / 115200 baud - 1 = 643
        BAUD_DIV = 643;
    }

    PORT {
        IN  [1] clk;
        IN  [1] rst_n;
        IN  [8] data;
        IN  [1] valid;
        OUT [1] ready;
        OUT [1] tx;
    }

    REGISTER {
        // State machine (0=IDLE, 1=START, 2=DATA, 3=STOP)
        state     [2] = 2'd0;
        baud_cnt  [10] = 10'd0;
        bit_cnt   [3] = 3'd0;
        shift     [8] = 8'hFF;

        // Outputs
        tx_out    [1] = 1'b1;
        ready_out [1] = 1'b1;
    }

    ASYNCHRONOUS {
        tx    <= tx_out;
        ready <= ready_out;
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        SELECT (state) {
            CASE (2'd0) {
                // IDLE: line high, ready for data
                tx_out <= 1'b1;
                IF (valid == 1'b1) {
                    shift     <= data;
                    baud_cnt  <= lit(10, BAUD_DIV);
                    state     <= 2'd1;
                    ready_out <= 1'b0;
                } ELSE {
                    ready_out <= 1'b1;
                }
            }

            CASE (2'd1) {
                // START bit: hold TX low for one baud period
                tx_out    <= 1'b0;
                ready_out <= 1'b0;
                IF (baud_cnt == 10'd0) {
                    baud_cnt <= lit(10, BAUD_DIV);
                    bit_cnt  <= 3'd0;
                    state    <= 2'd2;
                } ELSE {
                    baud_cnt <= baud_cnt - 10'd1;
                }
            }

            CASE (2'd2) {
                // DATA: shift out 8 bits LSB first
                tx_out    <= shift[0];
                ready_out <= 1'b0;
                IF (baud_cnt == 10'd0) {
                    shift <= { 1'b1, shift[7:1] };
                    IF (bit_cnt == 3'd7) {
                        baud_cnt <= lit(10, BAUD_DIV);
                        state    <= 2'd3;
                    } ELSE {
                        bit_cnt  <= bit_cnt + 3'd1;
                        baud_cnt <= lit(10, BAUD_DIV);
                    }
                } ELSE {
                    baud_cnt <= baud_cnt - 10'd1;
                }
            }

            CASE (2'd3) {
                // STOP bit: hold TX high for one baud period
                tx_out <= 1'b1;
                IF (baud_cnt == 10'd0) {
                    state     <= 2'd0;
                    ready_out <= 1'b1;
                } ELSE {
                    ready_out <= 1'b0;
                    baud_cnt  <= baud_cnt - 10'd1;
                }
            }

            DEFAULT {
                tx_out    <= 1'b1;
                ready_out <= 1'b1;
                state     <= 2'd0;
            }
        }
    }
@endmod
jz
@module por
    PORT {
        IN  [1] clk;
        IN  [1] done;
        OUT [1] por_n;
    }

    CONST {
        POR_CYCLES   = 16;
        POR_CNT_BITS = clog2(POR_CYCLES);
        POR_MAX      = POR_CYCLES - 1;
    }

    REGISTER {
        por_reg [1] = 1'b0;
        cnt     [POR_CNT_BITS] = POR_CNT_BITS'b0;
    }

    ASYNCHRONOUS {
        por_n <= por_reg;
    }

    SYNCHRONOUS(CLK=clk) {
        IF (done == 1'b0) {
            por_reg <= 1'b0;
            cnt <= POR_CNT_BITS'b0;
        } ELIF (cnt == lit(POR_CNT_BITS, POR_MAX)) {
            por_reg <= 1'b1;
            cnt <= cnt;
        } ELSE {
            por_reg <= 1'b0;
            cnt <= cnt + POR_CNT_BITS'b1;
        }
    }
@endmod

JZ-HDL Language Features

SDRAM tristate control. The 32-bit sdram_dq bus uses INOUT ports — the SDRAM controller drives during writes and releases to high-Z during reads. The compiler proves at compile time that no two modules drive the bus simultaneously.

Multi-client bus arbitration. The SDRAM arbiter multiplexes three independent clients using priority-based selection. Each client communicates through explicit request/acknowledge signals — no implicit bus sharing or synthesis-dependent arbitration.

Compressed data in BSRAM. Font data is stored compressed in BSRAM (initialized via @file) and decompressed to SDRAM at startup. The compiler embeds binary data directly into block RAM initialization, replacing vendor-specific memory initialization files.

I2C open-drain modeling. The EDID reader uses INOUT for SDA — driving low for logic 0 and releasing to high-Z for logic 1 (open-drain). The compiler tracks tristate ownership to prevent bus contention between the reader and the monitor's pull-up.