Skip to content

ps2_decoder_tdv

Source: Verilog/Terminals/rtl/ps2_decoder_tdv.v

Where it sits (Nexys): nd120_nexys4ddr_top > ps2_keyboard_tdv > ps2_decoder_tdv - instance path: KEYBOARD.DECODER

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

Contains: ps2_ascii_table_tdv

Module hierarchy - All modules

ps2_decoder_tdv symbol

Schematic

Drawn from the Verilog: the yosys netlist of the Nexys 4 DDR build, instance KEYBOARD.DECODER. 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).

ps2_decoder_tdv schematic

Ports

Direction Width Name Description
input 1 clk
input 1 rst_n (active low) async reset, active low
input 1 code_valid strobe, one clock
input [7:0] code_data the scancode itself
input 1 code_release this was a key RELEASE (F0 seen)
input 1 code_extended this had the E0 prefix
input 1 layout_no
output 1 ascii_valid one clock per character produced
output [7:0] ascii_data
output 1 shift_active
output 1 ctrl_active
output 1 caps_active
output 1 alt_active

Verilog source

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

Show the Verilog of ps2_decoder_tdv (141 lines)
//============================================================================
//! PS/2 scancode stream -> terminal bytes, TDV2200 table. Modifier state
//! and lookup only.
//!
//! Part of the board-independent terminal core (Verilog/Terminals/).
//! Thin sibling of ps2_decoder.v (VT100) - IDENTICAL logic, the only
//! difference is which ascii table it instantiates. ps2_decoder.v hardcodes
//! the instance name `ps2_ascii_table`, so it cannot be reused directly for
//! a second table without either a compile-time table-select inside that
//! file (risking the tested VT100 path) or this sibling. Per the decision
//! to keep VT100 and TDV2200 as two separate, compile-time-selected
//! modules (not a shared/branching implementation), this is the sibling.
//!
//! Written 31-AUG-2026.
//============================================================================

`default_nettype none

module ps2_decoder_tdv (
    input wire clk,
    input wire rst_n,  //! async reset, active low

    //! One scancode, already framed and stripped of its E0/F0 prefixes.
    input wire       code_valid,     //! strobe, one clock
    input wire [7:0] code_data,      //! the scancode itself
    input wire       code_release,   //! this was a key RELEASE (F0 seen)
    input wire       code_extended,  //! this had the E0 prefix

    //! 0 = US ANSI, 1 = Norwegian. See ps2_ascii_table_tdv.v.
    input wire       layout_no,

    output reg       ascii_valid,  //! one clock per character produced
    output reg [7:0] ascii_data,

    //! Modifier state, exposed because a board may want to light a lamp.
    output wire shift_active,
    output wire ctrl_active,
    output wire caps_active,
    output wire alt_active
);

  localparam [7:0] SC_LSHIFT = 8'h12;
  localparam [7:0] SC_RSHIFT = 8'h59;
  localparam [7:0] SC_CTRL   = 8'h14;
  localparam [7:0] SC_CAPS   = 8'h58;
  //! Left Alt is a plain (non-extended) 0x11; Right Alt arrives as E0 11.
  //! Both held to the same modifier bit - there is no separate AltGr
  //! behaviour implemented, matching Ctrl's single bit above.
  localparam [7:0] SC_ALT    = 8'h11;

  reg s_lshift, s_rshift, s_ctrl, s_caps, s_lalt, s_ralt;

  assign shift_active = s_lshift | s_rshift;
  assign ctrl_active  = s_ctrl;
  assign caps_active  = s_caps;
  assign alt_active   = s_lalt | s_ralt;

  wire s_shifted = s_lshift | s_rshift;

  wire [7:0] s_ascii_unshifted;
  wire [7:0] s_ascii_shifted;
  wire [7:0] s_ascii_extended;
  wire [7:0] s_alt_marker;

  ps2_ascii_table_tdv TABLE (
      .code      (code_data),
      .layout_no (layout_no),
      .unshifted (s_ascii_unshifted),
      .shifted   (s_ascii_shifted),
      .extended  (s_ascii_extended),
      .alt_marker(s_alt_marker)
  );

  //! Caps lock affects letters only - not digits, not punctuation.
  wire s_is_letter   = (s_ascii_unshifted >= "a") && (s_ascii_unshifted <= "z");
  wire s_use_shifted = s_shifted ^ (s_caps && s_is_letter);

  wire [7:0] s_ascii_plain = s_use_shifted ? s_ascii_shifted : s_ascii_unshifted;

  //! Control characters: CTRL + @A-Z[\]^_ gives 0x00-0x1F. Applied to the
  //! UNSHIFTED letter so ctrl-C and ctrl-shift-C are the same, as on a real
  //! terminal.
  wire [7:0] s_ascii_ctrl = {3'b000, s_ascii_unshifted[4:0]};

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      s_lshift    <= 1'b0;
      s_rshift    <= 1'b0;
      s_ctrl      <= 1'b0;
      s_caps      <= 1'b0;
      s_lalt      <= 1'b0;
      s_ralt      <= 1'b0;
      ascii_valid <= 1'b0;
      ascii_data  <= 8'h00;
    end else begin
      ascii_valid <= 1'b0;

      if (code_valid) begin
        case (code_data)
          SC_LSHIFT: s_lshift <= !code_release;
          SC_RSHIFT: s_rshift <= !code_release;
          SC_CTRL:   s_ctrl   <= !code_release;
          SC_CAPS:   if (!code_release) s_caps <= !s_caps;
          SC_ALT: if (code_extended) s_ralt <= !code_release;
                  else               s_lalt <= !code_release;

          default: begin
            if (!code_release) begin
              // Alt+key: a dedicated TDV application/editing-key marker,
              // never the plain letter/digit underneath. A key with no
              // Alt binding sends NOTHING while Alt is held, rather than
              // falling through to the plain character - holding Alt and
              // typing should not leak stray text.
              if (alt_active && !code_extended && s_alt_marker != 8'h00) begin
                ascii_valid <= 1'b1;
                ascii_data  <= s_alt_marker;
              end else if (alt_active && !code_extended) begin
                // no Alt binding for this key: send nothing
              end else if (code_extended) begin
                if (s_ascii_extended != 8'h00) begin
                  ascii_valid <= 1'b1;
                  ascii_data  <= s_ascii_extended;
                end
              end else if (s_ascii_plain != 8'h00) begin
                ascii_valid <= 1'b1;
                // A sequence MARKER (bit 7) is never ctrl-masked: there is
                // no Ctrl-Fn encoding on a TDV either (the F-key ESC[nn_
                // family already covers Shift; Ctrl is not modeled).
                ascii_data  <= (s_ctrl && !s_ascii_plain[7]) ? s_ascii_ctrl
                                                             : s_ascii_plain;
              end
            end
          end
        endcase
      end
    end
  end

endmodule

`default_nettype wire