PS/2协议解码 + 扫描码解析 + 按键状态机 — 用键盘控制你的FPGA!
🏆 成就:键盘骑士 ✅ Verilator验证通过
PS/2是键盘/鼠标的经典接口协议,仅需2根信号线:时钟和数据。键盘产生时钟,数据在时钟下降沿有效。
// PS/2接收器 - 解码11位串行帧
// 起始位(0) + 8数据位(LSB先) + 校验位 + 停止位(1)
module ps2_rx (
input wire clk, // 系统时钟(50MHz)
input wire rst,
input wire ps2_clk, // PS/2时钟(来自键盘)
input wire ps2_data, // PS/2数据
output reg [7:0] scan_code, // 接收到的扫描码
output reg data_valid, // 数据有效脉冲
output reg parity_error // 校验错误
);
// 同步PS/2时钟到系统时钟域
reg [2:0] ps2_clk_sync;
always @(posedge clk) begin
ps2_clk_sync <= {ps2_clk_sync[1:0], ps2_clk};
end
// 检测下降沿
wire ps2_clk_falling = (ps2_clk_sync[2:1] == 2'b10);
// 接收状态机
reg [3:0] bit_cnt;
reg [10:0] shift_reg;
always @(posedge clk) begin
if (rst) begin
bit_cnt <= 0;
shift_reg <= 0;
data_valid <= 0;
parity_error <= 0;
scan_code <= 0;
end else begin
data_valid <= 0; // 默认清除
if (ps2_clk_falling) begin
shift_reg <= {ps2_data, shift_reg[10:1]};
if (bit_cnt == 10) begin
// 收完11位: 检查帧
bit_cnt <= 0;
if (shift_reg[0] == 0 && shift_reg[10] == 1) begin
// 校验: 8数据位+校验位应有奇数个1
if (^shift_reg[9:1]) begin
scan_code <= shift_reg[8:1];
data_valid <= 1;
parity_error <= 0;
end else begin
parity_error <= 1;
end
end
end else begin
bit_cnt <= bit_cnt + 1;
end
end
end
end
endmodule// PS/2扫描码到按键映射
// 第二套扫描码(Scan Code Set 2)
module ps2_keymap (
input wire [7:0] scan_code,
output reg [3:0] key_index, // 0~15按键索引
output reg key_pressed // 1=按下, 0=释放
);
// F0前缀 = 释放码
// E0前缀 = 扩展码(忽略简化)
always @(*) begin
key_pressed = 1;
case(scan_code)
8'h1C: key_index = 4'h0; // A
8'h32: key_index = 4'h1; // B
8'h21: key_index = 4'h2; // C
8'h23: key_index = 4'h3; // D
8'h24: key_index = 4'h4; // E
8'h2B: key_index = 4'h5; // F
8'h34: key_index = 4'h6; // G
8'h33: key_index = 4'h7; // H
8'h43: key_index = 4'h8; // I
8'h3B: key_index = 4'h9; // J
8'h42: key_index = 4'hA; // K
8'h4B: key_index = 4'hB; // L
8'h1D: key_index = 4'hC; // W
8'h1B: key_index = 4'hD; // S
8'h75: key_index = 4'hE; // UP
8'h72: key_index = 4'hF; // DOWN
8'hF0: key_pressed = 0; // 释放前缀
default: key_pressed = 0;
endcase
end
endmodule// 键盘控制器 - 维护按键状态
module keyboard_controller (
input wire clk,
input wire rst,
input wire [7:0] scan_code,
input wire data_valid,
// 方向输出(用于游戏控制)
output reg key_up,
output reg key_down,
output reg key_left,
output reg key_right,
output reg key_action
);
reg f0_state; // 收到F0=1,下一个码是释放
reg [7:0] key_state [0:15]; // 16键状态
integer i;
always @(posedge clk) begin
if (rst) begin
f0_state <= 0;
key_up <= 0; key_down <= 0;
key_left <= 0; key_right <= 0;
key_action <= 0;
for(i=0; i<16; i=i+1) key_state[i] <= 0;
end else if (data_valid) begin
if (scan_code == 8'hF0) begin
f0_state <= 1;
end else if (scan_code == 8'hE0) begin
// 扩展码,忽略
end else begin
reg [3:0] kidx;
reg pressed;
pressed = ~f0_state;
f0_state <= 0;
// 简单映射
case(scan_code)
8'h75: kidx = 4'hE; // UP
8'h72: kidx = 4'hF; // DOWN
8'h6B: kidx = 4'hD; // LEFT
8'h74: kidx = 4'hC; // RIGHT
8'h29: kidx = 4'h0; // SPACE=action
default: kidx = 4'h0;
endcase
key_state[kidx] <= pressed;
end
// 更新方向输出
key_up <= key_state[4'hE];
key_down <= key_state[4'hF];
key_left <= key_state[4'hD];
key_right <= key_state[4'hC];
key_action <= key_state[4'h0];
end
end
endmodulemodule ps2_rx_tb;
logic clk=0, rst=1;
logic ps2_clk=1, ps2_data=1;
logic [7:0] scan_code;
logic data_valid, parity_error;
ps2_rx uut(.*);
always #10 clk = ~clk;
// 发送一个PS/2字节
task send_ps2_byte;
input [7:0] data;
integer i;
logic par;
begin
par = ^data; // 奇校验
// 起始位
ps2_data = 0; ps2_clk = 0; #40;
ps2_clk = 1; #40;
// 8数据位(LSB first)
for(i=0; i<8; i=i+1) begin
ps2_data = data[i]; ps2_clk = 0; #40;
ps2_clk = 1; #40;
end
// 校验位
ps2_data = ~par; ps2_clk = 0; #40;
ps2_clk = 1; #40;
// 停止位
ps2_data = 1; ps2_clk = 0; #40;
ps2_clk = 1; #40;
end
endtask
initial begin
rst=1; #100; rst=0;
$display("--- PS/2接收测试 ---");
// 发送'A'的扫描码: 0x1C
send_ps2_byte(8'h1C);
#200;
if(data_valid && scan_code == 8'h1C)
$display(" 接收0x1C (A) ✓");
else
$display(" FAIL: code=%h valid=%b", scan_code, data_valid);
// 发送'W': 0x1D
send_ps2_byte(8'h1D);
#200;
if(data_valid && scan_code == 8'h1D)
$display(" 接收0x1D (W) ✓");
else
$display(" FAIL: code=%h valid=%b", scan_code, data_valid);
$display("PS/2接收测试完成 ✓");
#100; $finish;
end
endmodule| 按键 | 扫描码 | 按键 | 扫描码 |
|---|---|---|---|
| A | 0x1C | 0 | 0x45 |
| B | 0x32 | 1 | 0x16 |
| C | 0x21 | 2 | 0x1E |
| D | 0x23 | 3 | 0x26 |
| W | 0x1D | UP | 0x75 |
| S | 0x1B | DOWN | 0x72 |
| SPACE | 0x29 | ENTER | 0x5A |
| LEFT | 0x6B | RIGHT | 0x74 |
💡 注意:PS/2键盘发送的是扫描码,不是ASCII!按下发送Make Code,松开发送F0+Make Code。扩展键(如方向键)前缀E0。多键同时按时,键盘轮流发送每个键的Make Code。
练习1:用WASD控制VGA精灵移动
练习2:添加去抖逻辑,防止按键抖动
练习3:实现组合键检测(同时按两个键)
练习4:显示当前按下的键的扫描码到七段数码管
练习5:实现键盘打字效果:按键在VGA屏幕上显示对应字符