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 PASSEDThe 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 novalid, and that the receiver recovers and captures the next good frame afterwards.This variant reuses the receiver and adds a decoder stage. A received
0xF0byte is swallowed and sets a one-shot flag; the next non-prefix byte is emitted withreleased=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 endmodulePASS 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 PASSEDThe log shows a make code producing
released=0, a lone0xF0prefix producing no event, and an0xF0-then-code sequence producing areleased=1event 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_clkat roughly 10-16.7 kHz and guaranteesps2_datais 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 intoclk_s_prevevery system cycle, andfalling = clk_s_prev & ~clk_sis true for exactly oneclkcycle 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_clkis 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-cyclefallingpulse comfortably captures one stable data bit per keyboard clock. The same applies tops2_datain 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
0xF0followed by that same make code. So pressing and releasing 'A' (scan set 2 code0x1C) produces1CthenF0 1C. The decoder in Example 2 handles exactly this: it recognizes0xF0, suppresses the event, and tags the following byte as a release. Extended keys additionally use an0xE0prefix (and release asE0 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_CLKlow to inhibit the device and then drivePS2_DATAto 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, the0xF0break 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 LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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 Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres