Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

PS/2 Keyboard Controller

Motivation

DOOM requires real-time keyboard input to play: movement (arrow keys), weapon switching, menu navigation, and action commands (door opening). The original HaDes-V provided switches and buttons, but these are insufficient for the full range of DOOM controls (albeit technically playable with creative mapping up the included switches, though that would not be a very enjoyable playing experience). A PS/2 keyboard interface provides the full keyboard matrix, enabling standard FPS controls (for the era).

PS/2 Receiver (ps2_keyboard.sv)

The PS/2 receiver decodes the asynchronous PS/2 protocol and buffers received scancodes in a hardware FIFO.

Clock Domain Crossing

The PS/2 clock (typically 10–16 kHz) and data signals are asynchronous to the 75 MHz CPU clock. A two-stage flip-flop synchronizer brings both signals into the system clock domain:

synchronizer sync_clk (
    .clk(clk),
    .async_in(ps2_clk),
    .sync_out(ps2_clk_sync)
);

The synchronized clock is fed into a falling-edge detector using a one-cycle delayed version:

assign ps2_clk_falling_edge = (ps2_clk_sync_prev == 1'b1) && (ps2_clk_sync == 1'b0);

Shift Register & Frame Decoding

On each falling edge of the synchronized PS/2 clock, the data bit is shifted into an 11-bit register:

shift_reg <= {ps2_data_sync, shift_reg[10:1]};

PS/2 frames consist of 11 bits: 1 start bit (0), 8 data bits (LSB-first), 1 odd parity bit, and 1 stop bit (1). After 11 bits, the receiver validates the frame:

if (shift_reg[1] == 1'b0 && ps2_data_sync == 1'b1) begin
    scancode <= shift_reg[9:2]; // Extract the 8 data bits
    frame_valid <= 1'b1;
end

The start bit is checked to be 0 (shift_reg[1]) and the stop bit (ps2_data_sync at bit count 10) to be 1. The 8 data bits are extracted from bits [9:2] of the shift register.

Watchdog Timer

Some keyboards can enter a state where they stop toggling the clock line mid-frame, leaving the receiver in a partial shift state. A watchdog timer monitors for clock inactivity:

if (idle_timer < 16'd50000) begin
    idle_timer <= idle_timer + 1;
end else begin
    bit_count <= 0; // Force sync reset!
end

If no falling edge is detected for 50,000 clock cycles (~0.67 ms at 75 MHz), the bit counter is reset, allowing the receiver to re-synchronize on the next frame.

Hardware FIFO

A 16-entry FIFO buffers received scancodes, preventing data loss if the CPU is busy when keystrokes arrive:

if (push) begin
    fifo_mem[wr_ptr] <= scancode;
    wr_ptr <= wr_ptr + 1;
end
if (pop && !empty) begin
    rd_ptr <= rd_ptr + 1;
end

The FIFO is configured with depth 16, sufficient to absorb burst input even during rendering-heavy frames.

Wishbone Interface (wishbone_ps2.sv)

The PS/2 receiver is wrapped with a simple Wishbone slave providing two registers:

OffsetNameAccessDescription
0x00DATARScancode byte (auto-pops FIFO on read)
0x04STATUSRBit 0: empty (1 = FIFO empty)

Reading the DATA register when the FIFO is non-empty pops the oldest scancode and returns it. The STATUS register reports the FIFO’s empty flag for polling-based drivers.

Software Driver (keyboard.c)

The software layer provides simple functions for the DOOM HAL:

  • keyboard_has_data(): Polls the STATUS register’s empty flag
  • keyboard_get_scancode(): Reads the DATA register (non-blocking)
  • keyboard_wait_for_scancode(): Busy-waits for a keystroke
  • keyboard_scancode_to_ascii(): Translates PS/2 Set 2 scan codes to ASCII characters

The scancode-to-ASCII table covers all letters (A–Z), digits (0–9), and common control keys (space, enter, escape, backspace, tab). Special keys (arrows, modifiers) have their own defines in keyboard.h and are handled by DOOM’s input layer in hades_v_doom.c.