⌨️ 第7课:PS/2键盘

PS/2协议时序 + 扫描码解析

🏆 成就:键盘侠 ✅ Verilator验证

⌨️
键盘侠
Verilator仿真读取PS/2扫描码

📡 PS/2数据帧

PS/2每次按键发送11位:起始位(0) + 8数据位(LSB first) + 奇校验 + 停止位(1)。时钟由键盘产生,FPGA在CLK下降沿读取DATA。

Verilogps2_rx.v核心
// 两级同步防亚稳态
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

📐 PS/2数据帧详解

PS/2数据帧 (11位): CLK ──┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌── │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ DATA ──┘ │D0│D1│D2│D3│D4│D5│D6│D7│P │ │ │ ↑ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ │ └─┘ START PARITY STOP 0 ←── 8位数据 (LSB first) ──→ 奇校验 1 在CLK的下降沿读取DATA! 时钟由键盘产生,FPGA只负责采样。 PS/2时钟频率约10~16.7kHz

🔑 扫描码表

按键Make码Break码
A1CF0,1C
B32F0,32
Enter5AF0,5A
Esc76F0,76
E0,75E0,F0,75
E0,72E0,F0,72
E0,6BE0,F0,6B
E0,74E0,F0,74

💡 关键理解

Make码:按下按键时发送,按住不放会重复发送

Break码:松开按键时发送,格式为 F0 + 扫描码

扩展码:方向键等扩展键以 E0 开头

📐 亚稳态与同步器

⚡ 为什么需要两级同步?

PS/2的CLK和DATA与FPGA系统时钟完全异步。直接采样可能采到信号跳变中间——亚稳态。两级DFF同步器将亚稳态概率降到几乎为零。这是FPGA设计的基本规则

📐 两级同步器详解

为什么需要两级同步? 外部PS/2信号 ──────┐ │ 异步! 系统时钟采样 ──────┘ 第一级DFF:可能采到亚稳态 输出 = 不确定(0? 1? 中间值?) 但经过一个时钟周期后,大概率稳定 第二级DFF:再次采样 输出 = 稳定的0或1 亚稳态概率降到几乎为零 实现: always @(posedge clk) begin sync0 <= ps2_clk; // 第一级 sync1 <= sync0; // 第二级(防亚稳态) end 这是FPGA设计的铁律: 任何外部异步信号 → 必须两级同步!

🧪 Verilator完整测试台

SystemVerilogps2_rx_tb.sv — 发送扫描码验证
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;
endmodule

🛠️ 编译运行

Bash编译运行步骤
verilator --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: 亚稳态影响? 两级同步器是必须的,不能用一级同步!

📏 PS/2协议关键参数

参数说明
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码
A1CF0,1C
B32F0,32
Enter5AF0,5A
Esc76F0,76
E0,75E0,F0,75
E0,72E0,F0,72

📐 亚稳态与同步器

⚡ 为什么需要两级同步? PS/2信号与FPGA系统时钟完全异步。如果直接采样,可能采到信号跳变中间——亚稳态。两级DFF同步器把亚稳态概率降到几乎为零。

🧪 Verilator测试台

SystemVerilogps2_rx_tb.sv片段
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

📐 PS/2数据帧详解

PS/2数据帧 (11位): CLK ──┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌── │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ DATA ──┘ │D0│D1│D2│D3│D4│D5│D6│D7│P │ │ │ ↑ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ │ └─┘ START PARITY STOP 0 ←── 8位数据 (LSB first) ──→ 奇校验 1 在CLK的下降沿读取DATA! 时钟由键盘产生,FPGA只负责采样。