Skip to content

key_vt100

Source: Verilog/Terminals/rtl/key_vt100.v

Hierarchy: not instantiated by any of the 9 build tops (elaborated by yosys).

Module hierarchy - All modules

key_vt100 symbol

Schematic

Drawn from the Verilog: no build top uses this module, so it was elaborated from its own file with no defines and default parameters. 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).

key_vt100 schematic

Ports

Direction Width Name Description
input 1 clk
input 1 rst_n (active low)
input 1 key_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_vt100.v on GitHub.

Show the Verilog of key_vt100 (205 lines)
//============================================================================
//! VT100 key-sequence expander - markers in, wire bytes out
//!
//! Part of the board-independent terminal core (Verilog/Terminals/).
//!
//! Sits between the keyboard decoder and the console UART TX. Ordinary bytes
//! pass through unchanged; a SEQUENCE MARKER (bit 7 set, written by
//! ps2_ascii_table.v) becomes a whole DEC escape sequence. The marker's
//! bits 6:5 select the family, bits 4:0 the payload
//! (docs/SPEC-vt100-keys.md):
//!
//!   100f_ffff  ->  ESC [ <0x40+f>   arrows (f 1..4 = A B C D)
//!   101f_ffff  ->  ESC O <0x40+f>   DEC PF1-PF4 (f 16..19 = P Q R S)
//!   110n_nnnn  ->  ESC [ <n> ~      editing keys n=1..6, F5-F12 n=15..24
//!                                   (n printed as one or two ASCII digits)
//!   111x_xxxx  ->  nothing          reserved, dropped
//!
//! This is what makes the keyboard speak VT100 (SINTRAN terminal type 6):
//! one keypress, three to five bytes on the wire.
//!
//! WHY THE FIFO. The UART needs ~870 us for a byte at 115200-7E1 and this
//! expander hands it up to five at once - without buffering, most of them
//! would be lost (which is why the single-byte TDV encoding never needed
//! this module). Sixteen entries is three worst-case sequences of headroom;
//! keypresses are milliseconds apart, so the FIFO in practice never holds
//! more than one sequence.
//!
//! HANDSHAKE, downstream side: console_uart_tx's contract is "byte_valid one
//! clock; ignored unless ready", with ready = idle. So this module pulses
//! out_valid for exactly one clock while out_ready is high, then waits for
//! ready to DROP (the byte was taken) and rise again (the frame finished)
//! before offering the next byte. If ready did not drop - which the current
//! UART cannot do, but a future one might - the byte is offered again rather
//! than lost.
//!
//! Upstream side: key_valid is a one-clock strobe with no backpressure (the
//! PS/2 decoder cannot wait). A marker takes up to five clocks to push; a
//! key landing INSIDE that window would be dropped - at 40 MHz that window
//! is ~125 ns against keypresses milliseconds apart, so it cannot happen
//! from a keyboard. A full FIFO also drops (and a full FIFO from typing
//! means the machine end is stuck, where dropped keys are the least of it).
//!
//! Written 30-AUG-2026.
//============================================================================

`default_nettype none

module key_vt100 (
    input wire clk,
    input wire rst_n,

    // From the keyboard decoder - strobes, no backpressure
    input wire       key_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 FIFO - 16 bytes, pointers one bit wider than the index so full and
  // empty are distinct (same construction as byte_fifo.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 five clocks. The marker cycle
  // pushes ESC and lines up the REST of the sequence in s_q0..s_q3; the
  // following cycles push those one per clock.
  //--------------------------------------------------------------------------
  localparam PU_IDLE = 1'b0;
  localparam PU_SEQ  = 1'b1;

  reg       s_push;
  reg [7:0] s_q0, s_q1, s_q2, s_q3;  //! the bytes after ESC
  reg [2:0] s_len;                   //! how many of them (2..4)
  reg [2:0] s_idx;

  //! The tilde family's parameter as one or two ASCII digits. n is 1..24 by
  //! construction of the table; a two-digit n has tens digit 1 or 2.
  wire [4:0] s_n     = key_data[4:0];
  wire       s_two   = (s_n >= 5'd10);
  wire [7:0] s_tens  = (s_n >= 5'd20) ? "2" : "1";
  wire [4:0] s_ones5 = (s_n >= 5'd20) ? (s_n - 5'd20)
                     : (s_n >= 5'd10) ? (s_n - 5'd10) : s_n;
  wire [7:0] s_ones  = {4'h3, s_ones5[3:0]};  //! ASCII digit

  //--------------------------------------------------------------------------
  // Pop side - one UART handshake at a time
  //--------------------------------------------------------------------------
  localparam [1:0] PO_IDLE = 2'd0;
  localparam [1:0] PO_TAKE = 2'd1;  //! offered; waiting to see ready drop

  reg [1:0] s_pop;
  reg       s_po_seen;  //! PO_TAKE has been through one full cycle

  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_len <= 3'd0; s_idx <= 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[6:5] != 2'b11) begin
              // A marker: ESC now, the rest lined up for PU_SEQ.
              s_mem[s_wptr[3:0]] <= ESC;
              s_wptr <= s_wptr + 5'd1;
              s_push <= PU_SEQ;
              s_idx  <= 3'd0;
              case (key_data[6:5])
                2'b00: begin  // ESC [ <final>
                  s_q0  <= "[";
                  s_q1  <= {2'b01, 1'b0, key_data[4:0]};  // 0x40 + payload
                  s_len <= 3'd2;
                end
                2'b01: begin  // ESC O <char>
                  s_q0  <= "O";
                  s_q1  <= {2'b01, 1'b0, key_data[4:0]};
                  s_len <= 3'd2;
                end
                default: begin  // 2'b10: ESC [ <n> ~
                  s_q0 <= "[";
                  if (s_two) begin
                    s_q1  <= s_tens;
                    s_q2  <= s_ones;
                    s_q3  <= "~";
                    s_len <= 3'd4;
                  end else begin
                    s_q1  <= s_ones;
                    s_q2  <= "~";
                    s_len <= 3'd3;
                  end
                end
              endcase
            end
            // family 111x_xxxx: reserved - dropped without a byte
          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_q3;
            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
          // TIMING, counted in edges. The offer was committed at edge k, so
          // valid is high during cycle k+1 and the UART accepts at edge k+1;
          // its busy flag is then visible (ready low) during cycle k+2 -
          // which is the SECOND PO_TAKE cycle. So: the first cycle here can
          // never see the drop and is skipped; from the second on, ready low
          // = taken (advance), ready still high = the UART ignored the pulse,
          // and PO_IDLE re-offers the same byte rather than losing it.
          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