key_tdv2200¶
Source: Verilog/Terminals/rtl/key_tdv2200.v
Where it sits (Nexys): nd120_nexys4ddr_top > key_tdv2200
- instance path: KEYEXP
Used in: nd120_console_mega65 (MEGA65 R6, MEGA65 R3), nd120_console_mister (MiSTer), nd120_nexys4ddr_top (Nexys)
Contains: no other modules.
Module hierarchy - All modules

Schematic¶
Drawn from the Verilog: the yosys netlist of the Nexys 4 DDR build, instance KEYEXP. Sub-modules are boxes (click the picture to open it full size; there every sub-module box links to its page, and every wire shows its Verilog name).
Ports¶
| Direction | Width | Name | Description |
|---|---|---|---|
| input | 1 |
clk |
|
| input | 1 |
rst_n (active low) |
|
| input | 1 |
key_valid |
one clock per character produced (from ps2_decoder_tdv.ascii_valid) |
| input | [7:0] |
key_data |
|
| output | 1 |
out_valid |
one clock, only while out_ready |
| output | [7:0] |
out_data |
|
| input | 1 |
out_ready |
the UART's "idle" flag |
Verilog source¶
Verilog/Terminals/rtl/key_tdv2200.v on GitHub.
Show the Verilog of key_tdv2200 (209 lines)
//============================================================================
//! TDV2200 key-sequence expander - markers in, wire bytes out
//!
//! Part of the board-independent terminal core (Verilog/Terminals/).
//! Sibling of key_vt100.v. Ordinary bytes (including the TDV's bare C0
//! cursor/Home/Delete codes - see ps2_ascii_table_tdv.v) pass through
//! unchanged; a SEQUENCE MARKER (bit 7 set) becomes a wire byte sequence.
//! TWO marker families, distinguished by range (see ps2_ascii_table_tdv.v's
//! header for exactly how each marker value is chosen):
//!
//! 0x80-0xC3 ESC [ <2-digit n> _ the F-key/registry family (shift =
//! n+1), always exactly 4 bytes after ESC, zero-padded.
//! 0xC4-0xDC Alt+key application/editing shortcuts - each marker has
//! its OWN fixed byte sequence (4 to 5 bytes after ESC; the
//! PUSH-key family runs to 5, including an EMBEDDED ESC as
//! plain data mid-sequence, harmless - the FIFO just moves
//! bytes, it never re-parses them).
//!
//! Same FIFO/handshake reasoning as key_vt100.v: the UART needs ~870 us per
//! byte at 115200-8N1 and this expander hands it up to 6 at once now (ESC +
//! up to 5), which is why the queue grew from 4 lookahead registers to 5.
//!
//! Written 31-AUG-2026.
//============================================================================
`default_nettype none
module key_tdv2200 (
input wire clk,
input wire rst_n,
// From the keyboard decoder - strobes, no backpressure
input wire key_valid, //! one clock per character produced (from ps2_decoder_tdv.ascii_valid)
input wire [7:0] key_data,
// To the console UART TX
output reg out_valid, //! one clock, only while out_ready
output reg [7:0] out_data,
input wire out_ready //! the UART's "idle" flag
);
localparam [7:0] ESC = 8'h1B;
//! The ESC[nn_ family's marker is 8'h80|n; n runs up to 86 in the
//! registry (NEWPARA) and Insert alone already uses 82 (marker 0xD2) -
//! NOT capped at 67. Everything from 0xE0 up is the Alt+key family
//! instead (ps2_ascii_table_tdv.v's ALTM_* localparams), chosen with
//! headroom past the highest n-based marker in use. key_data[7] alone
//! (used by key_vt100.v's sibling logic) is not enough here because
//! there are now two DIFFERENT marker shapes sharing the bit7 flag.
localparam [7:0] ALT_BASE = 8'hE0;
//--------------------------------------------------------------------------
// The FIFO - 16 bytes, pointers one bit wider than the index so full and
// empty are distinct (same construction as byte_fifo.v / key_vt100.v).
//--------------------------------------------------------------------------
reg [7:0] s_mem[0:15];
reg [4:0] s_wptr, s_rptr;
wire s_empty = (s_wptr == s_rptr);
wire s_full = (s_wptr[4] != s_rptr[4]) && (s_wptr[3:0] == s_rptr[3:0]);
//--------------------------------------------------------------------------
// Push side - expands a marker over up to 5 clocks: ESC now, up to 5 more
// bytes lined up in s_q0..s_q4 for the next PU_SEQ cycles, s_len says how
// many of them are real (4 for the ESC[nn_ family, 4 or 5 for Alt+key).
//--------------------------------------------------------------------------
localparam PU_IDLE = 1'b0;
localparam PU_SEQ = 1'b1;
reg s_push;
reg [7:0] s_q0, s_q1, s_q2, s_q3, s_q4;
reg [2:0] s_idx;
reg [2:0] s_len;
//! ESC[nn_ family: n is 0..67 by construction (bits[6:0] of the marker
//! when it is below ALT_BASE); always two ASCII decimal digits,
//! zero-padded.
wire [6:0] s_n = key_data[6:0];
wire [7:0] s_tens = "0" + (s_n / 7'd10);
wire [7:0] s_ones = "0" + (s_n % 7'd10);
//--------------------------------------------------------------------------
// Pop side - one UART handshake at a time (identical contract/timing to
// key_vt100.v - see that file's header for the full reasoning).
//--------------------------------------------------------------------------
localparam [1:0] PO_IDLE = 2'd0;
localparam [1:0] PO_TAKE = 2'd1;
reg [1:0] s_pop;
reg s_po_seen;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
s_wptr <= 5'd0;
s_rptr <= 5'd0;
s_push <= PU_IDLE;
s_q0 <= 8'd0; s_q1 <= 8'd0; s_q2 <= 8'd0; s_q3 <= 8'd0; s_q4 <= 8'd0;
s_idx <= 3'd0;
s_len <= 3'd0;
s_pop <= PO_IDLE;
out_valid <= 1'b0;
out_data <= 8'd0;
end else begin
out_valid <= 1'b0;
// ---- push ----
case (s_push)
PU_IDLE: begin
if (key_valid && !s_full) begin
if (!key_data[7]) begin
s_mem[s_wptr[3:0]] <= key_data;
s_wptr <= s_wptr + 5'd1;
end else if (key_data < ALT_BASE) begin
// ESC[nn_ family - see the header.
s_mem[s_wptr[3:0]] <= ESC;
s_wptr <= s_wptr + 5'd1;
s_push <= PU_SEQ;
s_idx <= 3'd0;
s_len <= 3'd4;
s_q0 <= "[";
s_q1 <= s_tens;
s_q2 <= s_ones;
s_q3 <= "_";
end else begin
// Alt+key family - each marker's own fixed sequence.
s_mem[s_wptr[3:0]] <= ESC;
s_wptr <= s_wptr + 5'd1;
// Default to staying idle (ESC alone, nothing queued) so an
// unrecognised marker can never leave s_len at 0 while
// s_push sits in PU_SEQ - "s_idx == s_len-1" would wrap
// 3'd0-1 to 3'd7 and PU_SEQ would never see it, pushing
// garbage until the FIFO fills. Every real case below
// overrides both.
s_push <= PU_IDLE;
s_idx <= 3'd0;
case (key_data)
8'hE0: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="4"; s_q2<="6"; s_q3<="_"; end // Alt+H HELP -> HJELP ESC[46_
8'hE1: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="2"; s_q2<="9"; s_q3<="~"; end // Alt+D DO (user-specified)
8'hE2: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="4"; s_q2<="2"; s_q3<="_"; end // Alt+U FUNC -> FUNK ESC[42_
8'hE3: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="4"; s_q2<="8"; s_q3<="_"; end // Alt+X EXIT -> SLUTT ESC[48_
8'hE4: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="3"; s_q2<="0"; s_q3<="_"; end // Alt+C CANCEL -> ANGRE ESC[30_
8'hE5: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="2"; s_q2<="6"; s_q3<="~"; end // Alt+M COMMAND (user-specified)
8'hE6: begin s_push<=PU_SEQ; s_len<=3'd5; s_q0<="["; s_q1<="1"; s_q2<=";"; s_q3<="2"; s_q4<="R"; end // Alt+F FIND (user-specified)
8'hE7: begin s_push<=PU_SEQ; s_len<=3'd5; s_q0<="["; s_q1<="4"; s_q2<=";"; s_q3<="2"; s_q4<="~"; end // Alt+S SELECT (user-specified)
8'hE8: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="1"; s_q2<="2"; s_q3<="_"; end // Alt+K COPY -> KOPI ESC[12_
8'hE9: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="1"; s_q2<="4"; s_q3<="_"; end // Alt+V MOVE -> FLYTT ESC[14_
8'hEA: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="2"; s_q2<="4"; s_q3<="_"; end // Alt+J JUST ESC[24_
8'hEB: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="0"; s_q2<="0"; s_q3<="_"; end // Alt+A MARK -> MERK ESC[00_
8'hEC: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="0"; s_q2<="2"; s_q3<="_"; end // Alt+L FIELD -> FELT ESC[02_
8'hED: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="0"; s_q2<="4"; s_q3<="_"; end // Alt+P PARA -> AVSH ESC[04_
8'hEE: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="0"; s_q2<="6"; s_q3<="_"; end // Alt+E SENT -> SETN ESC[06_
8'hEF: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="0"; s_q2<="8"; s_q3<="_"; end // Alt+W WORD -> ORD ESC[08_
8'hF0: begin s_push<=PU_SEQ; s_len<=3'd4; s_q0<="["; s_q1<="8"; s_q2<="2"; s_q3<="_"; end // Alt+I INSERT HERE -> INNS/EXPS ESC[82_ (approximate)
// PUSH1-8 (Alt+1..Alt+8): "ESC P N<digit> ESC \" - user-specified,
// unconfirmed (registry: PUSH keys have no fixed sequence at all).
8'hF1: begin s_push<=PU_SEQ; s_len<=3'd5; s_q0<="P"; s_q1<="N"; s_q2<="1"; s_q3<=ESC; s_q4<=8'h5C; end
8'hF2: begin s_push<=PU_SEQ; s_len<=3'd5; s_q0<="P"; s_q1<="N"; s_q2<="2"; s_q3<=ESC; s_q4<=8'h5C; end
8'hF3: begin s_push<=PU_SEQ; s_len<=3'd5; s_q0<="P"; s_q1<="N"; s_q2<="3"; s_q3<=ESC; s_q4<=8'h5C; end
8'hF4: begin s_push<=PU_SEQ; s_len<=3'd5; s_q0<="P"; s_q1<="N"; s_q2<="4"; s_q3<=ESC; s_q4<=8'h5C; end
8'hF5: begin s_push<=PU_SEQ; s_len<=3'd5; s_q0<="P"; s_q1<="N"; s_q2<="5"; s_q3<=ESC; s_q4<=8'h5C; end
8'hF6: begin s_push<=PU_SEQ; s_len<=3'd5; s_q0<="P"; s_q1<="N"; s_q2<="6"; s_q3<=ESC; s_q4<=8'h5C; end
8'hF7: begin s_push<=PU_SEQ; s_len<=3'd5; s_q0<="P"; s_q1<="N"; s_q2<="7"; s_q3<=ESC; s_q4<=8'h5C; end
8'hF8: begin s_push<=PU_SEQ; s_len<=3'd5; s_q0<="P"; s_q1<="N"; s_q2<="8"; s_q3<=ESC; s_q4<=8'h5C; end
default: ; // unrecognised marker: stays PU_IDLE, ESC alone sent (defensive only - never reached from ps2_ascii_table_tdv.v)
endcase
end
end
end
PU_SEQ: begin
if (!s_full) begin
s_mem[s_wptr[3:0]] <= (s_idx == 3'd0) ? s_q0
: (s_idx == 3'd1) ? s_q1
: (s_idx == 3'd2) ? s_q2
: (s_idx == 3'd3) ? s_q3 : s_q4;
s_wptr <= s_wptr + 5'd1;
if (s_idx == s_len - 3'd1) s_push <= PU_IDLE;
else s_idx <= s_idx + 3'd1;
end
end
default: s_push <= PU_IDLE;
endcase
// ---- pop ----
case (s_pop)
PO_IDLE: begin
s_po_seen <= 1'b0;
if (!s_empty && out_ready) begin
out_valid <= 1'b1;
out_data <= s_mem[s_rptr[3:0]];
s_pop <= PO_TAKE;
end
end
PO_TAKE: begin
if (!s_po_seen) begin
s_po_seen <= 1'b1;
end else begin
if (!out_ready) s_rptr <= s_rptr + 5'd1;
s_pop <= PO_IDLE;
end
end
default: s_pop <= PO_IDLE;
endcase
end
end
endmodule
`default_nettype wire