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_clkPLL 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_cacheModules
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;
}
@endprojjz
// 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;
}
}
@endmodjz
// 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;
}
}
@endmodjz
// 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);
}
}
}
@endmodjz
// 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;
}
@endmodjz
// 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);
}
}
}
@endmodjz
// 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);
}
}
}
@endmodjz
// 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);
}
}
}
@endmodjz
@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);
}
}
}
@endmodjz
// 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);
}
}
}
}
@endmodjz
// 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;
}
}
@endmodjz
// 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;
}
}
@endmodjz
// 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;
}
}
}
@endmodjz
// 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;
}
}
}
@endmodjz
@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;
}
}
@endmodJZ-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.