PS/2协议时序 + 扫描码解析
🏆 成就:键盘侠 ✅ Verilator验证
PS/2每次按键发送11位:起始位(0) + 8数据位(LSB first) + 奇校验 + 停止位(1)。时钟由键盘产生,FPGA在CLK下降沿读取DATA。
// 两级同步防亚稳态
reg ps2_clk_sync0, ps2_clk_sync1, ps2_clk_sync2;
always @(posedge clk) begin
ps2_clk_sync0<=ps2_clk;
ps2_clk_sync1<=ps2_clk_sync0;
ps2_clk_sync2<=ps2_clk_sync1;
end
wire ps2_falling=ps2_clk_sync2&~ps2_clk_sync1;
// 11位状态机:START→DATA(8bit)→PARITY→STOP
case(bit_count)
0: if(!ps2_data) bit_count<=1; // 起始位=0
1,2,3,4,5,6,7,8: begin // 数据位
shift_reg[bit_count-1]<=ps2_data;
bit_count<=bit_count+1;
end
9: begin shift_reg[8]<=ps2_data; bit_count<=10; end
10: if(ps2_data && (^shift_reg[7:0]^shift_reg[8])) begin
scan_code<=shift_reg[7:0]; scan_valid<=1;
end
endcase| 按键 | Make码 | Break码 |
|---|---|---|
| A | 1C | F0,1C |
| B | 32 | F0,32 |
| Enter | 5A | F0,5A |
| Esc | 76 | F0,76 |
| ↑ | E0,75 | E0,F0,75 |
| ↓ | E0,72 | E0,F0,72 |
| ← | E0,6B | E0,F0,6B |
| → | E0,74 | E0,F0,74 |
💡 关键理解:
• Make码:按下按键时发送,按住不放会重复发送
• Break码:松开按键时发送,格式为 F0 + 扫描码
• 扩展码:方向键等扩展键以 E0 开头
⚡ 为什么需要两级同步?
PS/2的CLK和DATA与FPGA系统时钟完全异步。直接采样可能采到信号跳变中间——亚稳态。两级DFF同步器将亚稳态概率降到几乎为零。这是FPGA设计的基本规则!
module ps2_rx_tb;
logic clk,rst_n; logic ps2_clk,ps2_data;
logic [7:0] scan_code; logic scan_valid;
ps2_rx uut(.*);
initial clk=0; always #10 clk=~clk;
localparam PS2_HALF=100;
task send_ps2_byte(input [7:0] data);
logic parity = ^data;
// 起始位
ps2_data=0;
#PS2_HALF; ps2_clk=0; #PS2_HALF; ps2_clk=1;
// 数据位D0~D7
for(int i=0;i<8;i++) begin
ps2_data=data[i];
#PS2_HALF; ps2_clk=0; #PS2_HALF; ps2_clk=1;
end
// 奇校验
ps2_data=~parity;
#PS2_HALF; ps2_clk=0; #PS2_HALF; ps2_clk=1;
// 停止位
ps2_data=1;
#PS2_HALF; ps2_clk=0; #PS2_HALF; ps2_clk=1;
#PS2_HALF;
endtask
initial begin
ps2_clk=1; ps2_data=1;
rst_n=0;#500;rst_n=1;#500;
$display("========== PS/2接收器验证 ==========");
send_ps2_byte(8'h1C); #2000; // A键
send_ps2_byte(8'h5A); #2000; // Enter
send_ps2_byte(8'hF0); #1000; // Break前缀
send_ps2_byte(8'h1C); #2000; // A键松开
$display("==========================================");
$finish;
end
initial #5_000_000 $finish;
endmoduleverilator --binary -j 0 --trace ps2_rx.v ps2_rx_tb.sv ./obj_dir/Vps2_rx_tb # 预期输出: # ========== PS/2接收器验证 ========== # ✓ 收到扫描码: 0x1C (A键) # ✓ 收到扫描码: 0x5A (Enter) # ✓ 收到扫描码: 0xF0 (Break前缀) # ✓ 收到扫描码: 0x1C (A键Break) # ==========================================
💡 实际硬件注意:PS/2时钟频率约10~16.7kHz,远慢于FPGA系统时钟(50MHz)。两级同步器的延迟(2个系统时钟周期=40ns)完全不影响PS/2信号采样(PS/2半周期≈30~50μs)。
✅ PS/2数据帧的11位分别是什么?
✅ 为什么在CLK下降沿采样DATA?
✅ 奇校验的原理是什么?
✅ Make码和Break码的区别?
✅ 为什么需要两级同步器?亚稳态是什么?
✅ 扩展键(方向键)的扫描码特征?
下一课进入声音世界!用PWM让FPGA播放音符。理解频率与音高的关系,用占空比控制音量。
Q: 收不到扫描码? 检查PS2时钟频率和仿真加速参数是否匹配。
Q: 校验错误? 奇校验:D0~D7中1的个数+校验位=奇数。计算公式:parity = ^data; check_bit = ~parity;
Q: 仿真太慢? 减小PS2_HALF参数加速仿真。实际PS2 15kHz,仿真可以用更高频率。
Q: 亚稳态影响? 两级同步器是必须的,不能用一级同步!
| 参数 | 值 | 说明 |
|---|---|---|
| CLK频率 | 10~16.7kHz | 由键盘产生 |
| CLK高/低时间 | 30~50μs | 半周期 |
| 数据帧长度 | 11位 | START+8DATA+PARITY+STOP |
| 校验方式 | 奇校验 | 1的个数+校验位=奇数 |
| 空闲状态 | CLK=1,DATA=1 | 两线都高 |
| 数据顺序 | LSB first | 最低位先发 |
| 通信方向 | 双向 | 主机也可发命令 |
💡 PS/2的历史:PS/2接口由IBM在1987年随PS/2系列电脑推出。虽然已被USB取代,但PS/2有一个USB没有的优势——中断驱动而非轮询,意味着PS/2键盘的按键响应延迟更低!这就是为什么电竞玩家仍然偏爱PS/2键盘。
| 按键 | Make码 | Break码 |
|---|---|---|
| A | 1C | F0,1C |
| B | 32 | F0,32 |
| Enter | 5A | F0,5A |
| Esc | 76 | F0,76 |
| ↑ | E0,75 | E0,F0,75 |
| ↓ | E0,72 | E0,F0,72 |
⚡ 为什么需要两级同步? PS/2信号与FPGA系统时钟完全异步。如果直接采样,可能采到信号跳变中间——亚稳态。两级DFF同步器把亚稳态概率降到几乎为零。
task send_ps2_byte(input [7:0] data);
logic parity = ^data;
// 起始位
ps2_data=0; #PS2_HALF; ps2_clk=0; #PS2_HALF; ps2_clk=1;
// 数据位D0~D7 (LSB first)
for(int i=0;i<8;i++) begin
ps2_data=data[i];
#PS2_HALF; ps2_clk=0; #PS2_HALF; ps2_clk=1;
end
// 奇校验位
ps2_data=~parity; #PS2_HALF; ps2_clk=0; #PS2_HALF; ps2_clk=1;
// 停止位
ps2_data=1; #PS2_HALF; ps2_clk=0; #PS2_HALF; ps2_clk=1;
endtask