PS/2 Keyboard Receiver in Verilog with Code

A PS/2 keyboard sends each keystroke as an 11-bit serial frame on two open-collector lines, PS2_CLK and PS2_DATA, clocked by the keyboard itself. This page gives you working PS/2 keyboard Verilog code: a receiver that synchronizes the asynchronous clock, samples data on the falling edge, verifies the start bit, odd parity and stop bit, and strobes out the 8-bit scancode.

Both examples below are complete, self-contained modules with self-checking testbenches that were compiled and simulated under Icarus Verilog in strict Verilog-2005 mode. The first is the clean receiver; the second layers on make/break decoding so a 0xF0 prefix turns the following byte into a key-release event.

This is the core PS/2 keyboard Verilog code: a two-flop synchronizer on ps2_clk, a falling-edge detector, an 11-bit shift register and a bit counter. When the stop bit arrives, framing and odd parity are checked and valid pulses for one clk cycle alongside the captured scancode.

// PS/2 keyboard receiver: samples ps2_data on the falling edge of the
// synchronized ps2_clk, shifts in the 11-bit frame (start, 8 data LSB-first,
// odd parity, stop), checks framing+parity and strobes `valid` with scancode.
module ps2_rx (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       ps2_clk,
    input  wire       ps2_data,
    output reg  [7:0] scancode,
    output reg        valid
);
    reg  [1:0]  clk_sync;      // 2-FF synchronizer for async ps2_clk
    reg         clk_s_prev;    // previous synchronized level (edge detect)
    reg  [10:0] shiftreg;      // received frame, LSB side is oldest bit
    reg  [3:0]  bitcount;      // 0..10
    reg  [10:0] frame;         // completed frame (combinational temp)

    wire clk_s   = clk_sync[1];
    wire falling = clk_s_prev & ~clk_s;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            clk_sync   <= 2'b11;
            clk_s_prev <= 1'b1;
            shiftreg   <= 11'd0;
            bitcount   <= 4'd0;
            scancode   <= 8'd0;
            valid      <= 1'b0;
        end else begin
            clk_sync   <= {clk_sync[0], ps2_clk};
            clk_s_prev <= clk_s;
            valid      <= 1'b0;             // one-cycle strobe
            if (falling) begin
                frame = {ps2_data, shiftreg[10:1]};   // next-state frame
                if (bitcount == 4'd10) begin
                    // 11th bit (stop) just arrived: frame is complete.
                    // frame[0]=start, frame[8:1]=data, frame[9]=parity, frame[10]=stop
                    if (frame[0] == 1'b0 && frame[10] == 1'b1 &&
                        (^frame[9:1]) == 1'b1) begin
                        scancode <= frame[8:1];
                        valid    <= 1'b1;
                    end
                    bitcount <= 4'd0;
                    shiftreg <= 11'd0;
                end else begin
                    shiftreg <= frame;
                    bitcount <= bitcount + 4'd1;
                end
            end
        end
    end
endmodule

module tb;
    reg        clk = 1'b0;
    reg        rst_n = 1'b0;
    reg        ps2_clk = 1'b1;
    reg        ps2_data = 1'b1;
    wire [7:0] scancode;
    wire       valid;

    integer    pass_count = 0;
    integer    fail_count = 0;

    ps2_rx dut (.clk(clk), .rst_n(rst_n), .ps2_clk(ps2_clk),
                .ps2_data(ps2_data), .scancode(scancode), .valid(valid));

    always #1 clk = ~clk;   // 2-time-unit clock period

    // Drive one PS/2 bit the way a device does: data stable while clock high,
    // host latches on the falling edge.
    task send_bit;
        input b;
        begin
            ps2_data = b;
            ps2_clk  = 1'b1; #20;
            ps2_clk  = 1'b0; #20;   // falling edge here
            ps2_clk  = 1'b1; #10;
        end
    endtask

    // Send a full frame; `par` is the parity bit actually transmitted.
    task send_frame;
        input [7:0] data;
        input       par;
        integer i;
        begin
            send_bit(1'b0);                 // start
            for (i = 0; i < 8; i = i + 1)
                send_bit(data[i]);          // 8 data bits, LSB first
            send_bit(par);                  // parity
            send_bit(1'b1);                 // stop
            #10;
        end
    endtask

    // Capture a `valid` strobe (if any) within a window.
    reg        got_valid;
    reg  [7:0] got_code;
    always @(posedge clk) begin
        if (valid) begin
            got_valid <= 1'b1;
            got_code  <= scancode;
        end
    end

    task expect_good;
        input [7:0] data;
        reg odd_par;
        begin
            odd_par = ~(^data);             // odd parity bit
            got_valid = 1'b0;
            send_frame(data, odd_par);
            if (got_valid && got_code == data) begin
                $display("PASS good frame 0x%02h captured, parity OK", data);
                pass_count = pass_count + 1;
            end else begin
                $display("FAIL good frame 0x%02h: valid=%b code=0x%02h",
                         data, got_valid, got_code);
                fail_count = fail_count + 1;
            end
        end
    endtask

    task expect_badparity;
        input [7:0] data;
        reg wrong_par;
        begin
            wrong_par = (^data);            // deliberately wrong (even) parity
            got_valid = 1'b0;
            send_frame(data, wrong_par);
            if (!got_valid) begin
                $display("PASS bad-parity frame 0x%02h rejected (no valid)", data);
                pass_count = pass_count + 1;
            end else begin
                $display("FAIL bad-parity frame 0x%02h asserted valid=0x%02h",
                         data, got_code);
                fail_count = fail_count + 1;
            end
        end
    endtask

    initial begin
        got_valid = 1'b0;
        got_code  = 8'd0;
        #8 rst_n = 1'b1;
        #10;

        expect_good(8'h1C);   // 'A' make code
        expect_good(8'h00);   // all-zero data (parity=1)
        expect_good(8'hFF);   // all-one data (parity=1)
        expect_good(8'h29);   // space
        expect_badparity(8'h5A);
        expect_good(8'h76);   // ESC make code, confirm recovery after a bad frame

        $display("RESULT %0d passed, %0d failed", pass_count, fail_count);
        if (fail_count == 0)
            $display("ALL TESTS PASSED");
        $finish;
    end
endmodule
PASS good frame 0x1c captured, parity OK
PASS good frame 0x00 captured, parity OK
PASS good frame 0xff captured, parity OK
PASS good frame 0x29 captured, parity OK
PASS bad-parity frame 0x5a rejected (no valid)
PASS good frame 0x76 captured, parity OK
RESULT 6 passed, 0 failed
ALL TESTS PASSED

The testbench acts as the keyboard, driving framed bytes with correct odd parity and latching on the falling edge of ps2_clk. It confirms four good frames (including all-zero and all-one data) are captured exactly, that a frame with deliberately wrong parity asserts no valid, and that the receiver recovers and captures the next good frame afterwards.

This variant reuses the receiver and adds a decoder stage. A received 0xF0 byte is swallowed and sets a one-shot flag; the next non-prefix byte is emitted with released=1, so a single key press/release pair becomes two clean events.

// PS/2 keyboard front-end that decodes make/break events.
// ps2_rx receives raw scancode bytes; ps2_key_decode turns a 0xF0 prefix
// followed by a code into a key-release event, and a bare code into a
// key-press event, emitting key_code + released + key_strobe.
module ps2_rx (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       ps2_clk,
    input  wire       ps2_data,
    output reg  [7:0] scancode,
    output reg        valid
);
    reg  [1:0]  clk_sync;
    reg         clk_s_prev;
    reg  [10:0] shiftreg;
    reg  [3:0]  bitcount;
    reg  [10:0] frame;

    wire clk_s   = clk_sync[1];
    wire falling = clk_s_prev & ~clk_s;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            clk_sync   <= 2'b11;
            clk_s_prev <= 1'b1;
            shiftreg   <= 11'd0;
            bitcount   <= 4'd0;
            scancode   <= 8'd0;
            valid      <= 1'b0;
        end else begin
            clk_sync   <= {clk_sync[0], ps2_clk};
            clk_s_prev <= clk_s;
            valid      <= 1'b0;
            if (falling) begin
                frame = {ps2_data, shiftreg[10:1]};
                if (bitcount == 4'd10) begin
                    if (frame[0] == 1'b0 && frame[10] == 1'b1 &&
                        (^frame[9:1]) == 1'b1) begin
                        scancode <= frame[8:1];
                        valid    <= 1'b1;
                    end
                    bitcount <= 4'd0;
                    shiftreg <= 11'd0;
                end else begin
                    shiftreg <= frame;
                    bitcount <= bitcount + 4'd1;
                end
            end
        end
    end
endmodule

// Decode make/break. A 0xF0 byte sets a one-shot "next code is a release".
module ps2_key_decode (
    input  wire       clk,
    input  wire       rst_n,
    input  wire [7:0] scancode,
    input  wire       valid,
    output reg  [7:0] key_code,
    output reg        released,
    output reg        key_strobe
);
    localparam BREAK_PREFIX = 8'hF0;
    reg break_pending;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            key_code      <= 8'd0;
            released      <= 1'b0;
            key_strobe    <= 1'b0;
            break_pending <= 1'b0;
        end else begin
            key_strobe <= 1'b0;
            if (valid) begin
                if (scancode == BREAK_PREFIX) begin
                    break_pending <= 1'b1;        // swallow prefix, no event yet
                end else begin
                    key_code      <= scancode;
                    released      <= break_pending;
                    key_strobe    <= 1'b1;
                    break_pending <= 1'b0;        // consumed
                end
            end
        end
    end
endmodule

module tb;
    reg        clk = 1'b0;
    reg        rst_n = 1'b0;
    reg        ps2_clk = 1'b1;
    reg        ps2_data = 1'b1;
    wire [7:0] scancode;
    wire       valid;
    wire [7:0] key_code;
    wire       released;
    wire       key_strobe;

    integer    pass_count = 0;
    integer    fail_count = 0;

    ps2_rx rx (.clk(clk), .rst_n(rst_n), .ps2_clk(ps2_clk),
               .ps2_data(ps2_data), .scancode(scancode), .valid(valid));
    ps2_key_decode dec (.clk(clk), .rst_n(rst_n), .scancode(scancode),
               .valid(valid), .key_code(key_code), .released(released),
               .key_strobe(key_strobe));

    always #1 clk = ~clk;

    task send_bit;
        input b;
        begin
            ps2_data = b;
            ps2_clk  = 1'b1; #20;
            ps2_clk  = 1'b0; #20;
            ps2_clk  = 1'b1; #10;
        end
    endtask

    task send_byte;                 // sends one framed byte with correct odd parity
        input [7:0] data;
        integer i;
        reg odd_par;
        begin
            odd_par = ~(^data);
            send_bit(1'b0);
            for (i = 0; i < 8; i = i + 1) send_bit(data[i]);
            send_bit(odd_par);
            send_bit(1'b1);
            #10;
        end
    endtask

    reg        ev_seen;
    reg  [7:0] ev_code;
    reg        ev_rel;
    always @(posedge clk) begin
        if (key_strobe) begin
            ev_seen <= 1'b1;
            ev_code <= key_code;
            ev_rel  <= released;
        end
    end

    // Expect exactly one event with the given code/release after sending bytes.
    task check_press;
        input [7:0] code;
        begin
            ev_seen = 1'b0;
            send_byte(code);
            if (ev_seen && ev_code == code && ev_rel == 1'b0) begin
                $display("PASS make 0x%02h -> key_code=0x%02h released=0", code, ev_code);
                pass_count = pass_count + 1;
            end else begin
                $display("FAIL make 0x%02h: seen=%b code=0x%02h rel=%b",
                         code, ev_seen, ev_code, ev_rel);
                fail_count = fail_count + 1;
            end
        end
    endtask

    task check_prefix_only;         // 0xF0 alone must NOT emit an event
        begin
            ev_seen = 1'b0;
            send_byte(8'hF0);
            if (!ev_seen) begin
                $display("PASS break prefix 0xF0 alone emits no event");
                pass_count = pass_count + 1;
            end else begin
                $display("FAIL prefix 0xF0 emitted event code=0x%02h", ev_code);
                fail_count = fail_count + 1;
            end
        end
    endtask

    task check_release;             // F0 then code => release event for code
        input [7:0] code;
        begin
            check_prefix_only;
            ev_seen = 1'b0;
            send_byte(code);
            if (ev_seen && ev_code == code && ev_rel == 1'b1) begin
                $display("PASS break 0xF0,0x%02h -> key_code=0x%02h released=1", code, ev_code);
                pass_count = pass_count + 1;
            end else begin
                $display("FAIL break 0x%02h: seen=%b code=0x%02h rel=%b",
                         code, ev_seen, ev_code, ev_rel);
                fail_count = fail_count + 1;
            end
        end
    endtask

    initial begin
        ev_seen = 1'b0; ev_code = 8'd0; ev_rel = 1'b0;
        #8 rst_n = 1'b1;
        #10;

        check_press(8'h1C);          // press 'A'
        check_release(8'h1C);        // release 'A'  (F0 1C)
        check_press(8'h32);          // press 'B'
        check_press(8'h76);          // press ESC, confirms pending flag cleared
        check_release(8'h76);        // release ESC

        $display("RESULT %0d passed, %0d failed", pass_count, fail_count);
        if (fail_count == 0)
            $display("ALL TESTS PASSED");
        $finish;
    end
endmodule
PASS make 0x1c -> key_code=0x1c released=0
PASS break prefix 0xF0 alone emits no event
PASS break 0xF0,0x1c -> key_code=0x1c released=1
PASS make 0x32 -> key_code=0x32 released=0
PASS make 0x76 -> key_code=0x76 released=0
PASS break prefix 0xF0 alone emits no event
PASS break 0xF0,0x76 -> key_code=0x76 released=1
RESULT 7 passed, 0 failed
ALL TESTS PASSED

The log shows a make code producing released=0, a lone 0xF0 prefix producing no event, and an 0xF0-then-code sequence producing a released=1 event for the right key. It also presses a new key after a release to prove the break-pending flag was cleared.

The PS/2 11-bit frame and odd parity

Every byte the keyboard sends is wrapped in an 11-bit frame transmitted least-significant-bit first:

  • Start bit - always 0.
  • 8 data bits - the scancode, LSB first.
  • Parity bit - odd parity: it is chosen so the total number of 1s across the 8 data bits plus the parity bit is odd.
  • Stop bit - always 1.

In Verilog the odd-parity bit the sender must transmit is simply ~(^data), and the receiver validates a frame by checking (^{parity, data}) == 1 - the XOR of all nine bits is 1 exactly when their number of ones is odd. Both testbenches compute parity this way rather than hard-coding it, so a wiring bug would surface as a FAIL.

Sampling on the ps2_clk falling edge

The keyboard is the clock master: it drives ps2_clk at roughly 10-16.7 kHz and guarantees ps2_data is stable around the falling edge. The host must read each bit on that falling edge. In the code this is a classic edge detector: the synchronized clock level is registered into clk_s_prev every system cycle, and falling = clk_s_prev & ~clk_s is true for exactly one clk cycle per falling transition. A bit counter from 0 to 10 tracks position in the frame; on the eleventh edge the completed frame is evaluated and the counter resets, ready for the next start bit.

Synchronizing the asynchronous PS/2 clock

ps2_clk is completely unrelated to your FPGA's system clock, so feeding it straight into logic that must make a single, consistent decision risks metastability. The receiver passes it through a two-stage flip-flop synchronizer (clk_sync) before any edge detection. Because the system clock (tens of MHz) is far faster than the PS/2 clock (tens of kHz), the single-cycle falling pulse comfortably captures one stable data bit per keyboard clock. The same applies to ps2_data in a hardened design; here data is only consumed on the already-synchronized falling edge, well after it has settled.

Common mistakes

  • Clocking logic directly on ps2_clk (using it as a true clock). It works in simulation but creates an unconstrained clock domain and fails timing closure on real silicon - treat PS/2 clock as data and sample it with your system clock.
  • Forgetting the synchronizer and detecting edges on the raw pin, which can double-count or miss edges due to metastability.
  • Getting bit order wrong - PS/2 is LSB-first; shifting MSB-first silently bit-reverses every scancode.
  • Not resetting the bit counter after the stop bit, so the receiver drifts out of frame alignment after the first byte.

Make codes versus break codes

Pressing a key sends its make code (for most keys a single byte). Releasing the key sends a break code: the byte 0xF0 followed by that same make code. So pressing and releasing 'A' (scan set 2 code 0x1C) produces 1C then F0 1C. The decoder in Example 2 handles exactly this: it recognizes 0xF0, suppresses the event, and tags the following byte as a release. Extended keys additionally use an 0xE0 prefix (and release as E0 F0 xx); the same one-shot-flag pattern extends naturally to cover it.

Debouncing and the host-to-device direction

You generally do not need to debounce PS/2 keystrokes in hardware - the keyboard's own controller already debounces the key matrix and implements typematic repeat, so the host just consumes clean scancodes. What you should guard against is electrical glitches on the line, which is what the synchronizer and the clean falling-edge detector handle.

The receivers here are one-directional (device-to-host), which covers reading keystrokes. Full PS/2 is bidirectional: the host can pull PS2_CLK low to inhibit the device and then drive PS2_DATA to send commands such as setting the caps/num-lock LEDs or changing the typematic rate. Because the lines are open-collector with pull-ups, host transmission means driving the line low or releasing it to be pulled high - never driving a hard 1. For keyboard input alone, the receive path above is all you need.

FAQ

Which scancode set does this assume? The framing and parity are identical for all sets; the example byte values (0x1C, 0x76, the 0xF0 break prefix) are from scan code set 2, the power-on default for PS/2 keyboards.

What clock frequency does the receiver need? Any system clock comfortably faster than the PS/2 clock works; a few MHz is plenty. The design samples with the system clock and does not care about the exact PS/2 rate.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD