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:
| Offset | Name | Access | Description |
|---|---|---|---|
| 0x00 | DATA | R | Scancode byte (auto-pops FIFO on read) |
| 0x04 | STATUS | R | Bit 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 flagkeyboard_get_scancode(): Reads the DATA register (non-blocking)keyboard_wait_for_scancode(): Busy-waits for a keystrokekeyboard_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.