⌨️ 第19课:PS/2键盘接口

📖 本课目标

设计并实现PS/2键盘接口控制器,掌握PS/2串行通信协议,实现扫描码的接收、奇偶校验和FIFO缓冲。PS/2是8位复古电脑最经典的键盘输入接口——理解它,你就理解了异步串行通信的核心原理。

🧠 PS/2协议深度解析

PS/2接口诞生于1987年的IBM PS/2系列电脑,虽然已被USB取代,但它的协议设计精巧、实现简单,是学习串行通信的绝佳案例。PS/2协议的核心特点:

PS/2数据帧格式 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ PS2_CLK ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ─────┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─── S D0 D1 D2 D3 D4 D5 D6 D7 P ST 起始 ←── 数据位(LSB先发)──→ 奇校验 停止 S=0(起始) P=奇校验 ST=1(停止) 键盘在PS2_CLK下降沿更新数据,主机在上升沿采样

PS/2时序参数

参数最小值典型值最大值
时钟频率10 KHz~15 KHz16.7 KHz
时钟高电平30 μs--
时钟低电平30 μs--
数据到时钟建立时间5 μs--
时钟到数据保持时间5 μs--
💡 关键理解:PS/2是"设备驱动时钟"的协议。键盘在按键时主动发送时钟和数据,主机被动接收。这与UART不同——UART没有时钟线,靠约定波特率。PS/2有独立时钟线,因此不需要约定速率,天然同步。

📋 扫描码体系

PS/2键盘发送的不是ASCII码,而是扫描码(Scan Code)。IBM兼容键盘使用三套扫描码集,最常用的是第二套(Scan Code Set 2):

扫描码集2的关键码表

按键通码(Make)断码(Break)说明
A1CF0 1C按下发1C,释放发F0+1C
B32F0 32断码前缀固定为F0
Enter5AF0 5A回车键
Space29F0 29空格键
Esc76F0 76ESC键
Left Shift12F0 12左Shift
Ctrl14F0 14控制键
ExtendedE0 xxE0 F0 xx扩展键前缀
扫描码类型总结 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. 通码 (Make Code):按键按下时发送 - 普通键:1字节(如 A = 1C) - 扩展键:E0 + 1字节(如 Right Alt = E0 11) 2. 断码 (Break Code):按键释放时发送 - 普通键:F0 + 通码(如 A释放 = F0 1C) - 扩展键:E0 F0 + 通码 3. 特殊键: - Pause:E1 14 77 E1 F0 14 F0 77(8字节,无断码) - Print Screen:E0 12 E0 7C(通码4字节) 4. 长按重复:持续发送通码(Typematic Repeat) - 默认延迟500ms后,每秒10.9次重复

🏗️ 控制器架构设计

我们的PS/2键盘控制器分为四个核心模块:

PS/2键盘控制器架构 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ┌──────────────┐ ps2_clk ─────────→│ │ ps2_data ─────────→│ 串行接收器 │──── scan_code[7:0] │ (状态机) │──── scan_ready │ │──── scan_error └──────────────┘ │ ▼ ┌──────────────┐ │ 扫描码解析 │──── ascii_code[7:0] │ (扩展码处理) │──── key_event (press/release) │ │──── extended_flag └──────────────┘ │ ▼ ┌──────────────┐ │ FIFO缓冲 │──── fifo_data[7:0] │ (16深度) │──── fifo_empty │ │──── fifo_full └──────────────┘ │ ▼ ┌──────────────┐ │ 内存映射接口 │─── $FF02 键盘数据 │ (CPU读取) │─── $FF03 键盘状态 └──────────────┘

🔧 Verilog实现

1. 串行接收器核心

串行接收器是整个控制器的基础——它负责从PS/2的串行比特流中提取出完整的8位扫描码:

// ps2_keyboard.v - PS/2键盘接口控制器
// 功能:接收PS/2串行协议数据,输出扫描码

module ps2_keyboard (
    input  wire        clk,          // 系统时钟
    input  wire        rst_n,        // 异步复位(低有效)
    input  wire        ps2_clk,      // PS/2时钟线
    input  wire        ps2_data,     // PS/2数据线
    output reg [7:0]  scan_code,    // 接收到的扫描码
    output reg        scan_ready,   // 扫描码就绪标志
    output reg        scan_error,   // 奇偶校验错误
    input  wire        scan_ack      // CPU应答(读取扫描码)
);

    // PS/2时钟同步(消除亚稳态)
    // 使用3级同步器,MTBF远超实际需求
    reg [2:0] ps2_clk_sync;
    reg [2:0] ps2_data_sync;

    always @(posedge clk) begin
        ps2_clk_sync  <= {ps2_clk_sync[1:0], ps2_clk};
        ps2_data_sync <= {ps2_data_sync[1:0], ps2_data};
    end

    // 下降沿检测:键盘在下降沿更新数据,我们在下降沿捕获
    wire ps2_clk_falling = (ps2_clk_sync[2:1] == 2'b10);

    // 接收状态机
    localparam IDLE   = 4'd0;
    localparam START  = 4'd1;
    localparam D0     = 4'd2;
    localparam D1     = 4'd3;
    localparam D2     = 4'd4;
    localparam D3     = 4'd5;
    localparam D4     = 4'd6;
    localparam D5     = 4'd7;
    localparam D6     = 4'd8;
    localparam D7     = 4'd9;
    localparam PARITY = 4'd10;
    localparam STOP   = 4'd11;

    reg [3:0] state;
    reg [7:0] shift_reg;   // 数据移位寄存器
    reg       parity_reg;  // 奇偶校验计算

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state      <= IDLE;
            shift_reg  <= 8'd0;
            parity_reg <= 1'b0;
            scan_code  <= 8'd0;
            scan_ready <= 1'b0;
            scan_error <= 1'b0;
        end else begin
            // 应答后清除就绪标志
            if (scan_ack && scan_ready)
                scan_ready <= 1'b0;

            if (ps2_clk_falling) begin
                case (state)
                    IDLE: begin
                        if (ps2_data_sync[2] == 1'b0) begin
                            state      <= D0;
                            shift_reg  <= 8'd0;
                            parity_reg <= 1'b0;
                            scan_error <= 1'b0;
                        end
                    end
                    D0: begin
                        shift_reg[0] <= ps2_data_sync[2];
                        parity_reg  <= parity_reg ^ ps2_data_sync[2];
                        state <= D1;
                    end
                    D1: begin
                        shift_reg[1] <= ps2_data_sync[2];
                        parity_reg  <= parity_reg ^ ps2_data_sync[2];
                        state <= D2;
                    end
                    D2: begin
                        shift_reg[2] <= ps2_data_sync[2];
                        parity_reg  <= parity_reg ^ ps2_data_sync[2];
                        state <= D3;
                    end
                    D3: begin
                        shift_reg[3] <= ps2_data_sync[2];
                        parity_reg  <= parity_reg ^ ps2_data_sync[2];
                        state <= D4;
                    end
                    D4: begin
                        shift_reg[4] <= ps2_data_sync[2];
                        parity_reg  <= parity_reg ^ ps2_data_sync[2];
                        state <= D5;
                    end
                    D5: begin
                        shift_reg[5] <= ps2_data_sync[2];
                        parity_reg  <= parity_reg ^ ps2_data_sync[2];
                        state <= D6;
                    end
                    D6: begin
                        shift_reg[6] <= ps2_data_sync[2];
                        parity_reg  <= parity_reg ^ ps2_data_sync[2];
                        state <= D7;
                    end
                    D7: begin
                        shift_reg[7] <= ps2_data_sync[2];
                        parity_reg  <= parity_reg ^ ps2_data_sync[2];
                        state <= PARITY;
                    end
                    PARITY: begin
                        // 奇校验:数据位+校验位 = 奇数个1
                        if (parity_reg ^ ps2_data_sync[2])
                            state <= STOP;
                        else begin
                            scan_error <= 1'b1;
                            state      <= IDLE;
                        end
                    end
                    STOP: begin
                        if (ps2_data_sync[2] == 1'b1) begin
                            scan_code  <= shift_reg;
                            scan_ready <= 1'b1;
                        end else
                            scan_error <= 1'b1;
                        state <= IDLE;
                    end
                    default: state <= IDLE;
                endcase
            end
        end
    end

endmodule

2. 扫描码解析器

接收到的原始扫描码需要解析出按键事件(按下/释放)和扩展键标志:

// ps2_scancode_parser.v - 扫描码解析器
// 功能:解析PS/2扫描码序列,识别通码/断码/扩展码

module ps2_scancode_parser (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [7:0]  scan_code,
    input  wire        scan_ready,
    output reg [7:0]  key_code,     // 按键码(去除F0/E0前缀)
    output reg        key_press,    // 1=按下 0=释放
    output reg        key_extended, // 扩展键标志
    output reg        key_valid     // 按键事件有效
);

    localparam NORMAL = 2'd0;  // 等待普通扫描码
    localparam F0     = 2'd1;  // 收到F0,等待断码
    localparam E0     = 2'd2;  // 收到E0,扩展键
    localparam E0_F0  = 2'd3;  // E0+F0,扩展键断码

    reg [1:0] state;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state        <= NORMAL;
            key_code     <= 8'd0;
            key_press    <= 1'b0;
            key_extended <= 1'b0;
            key_valid    <= 1'b0;
        end else begin
            key_valid <= 1'b0;  // 默认无效

            if (scan_ready) begin
                case (state)
                    NORMAL: begin
                        case (scan_code)
                            8'hF0: state <= F0;       // 断码前缀
                            8'hE0: state <= E0;       // 扩展键前缀
                            default: begin
                                key_code     <= scan_code;
                                key_press    <= 1'b1;
                                key_extended <= 1'b0;
                                key_valid    <= 1'b1;
                            end
                        endcase
                    end
                    F0: begin
                        key_code     <= scan_code;
                        key_press    <= 1'b0;
                        key_extended <= 1'b0;
                        key_valid    <= 1'b1;
                        state        <= NORMAL;
                    end
                    E0: begin
                        if (scan_code == 8'hF0)
                            state <= E0_F0;
                        else begin
                            key_code     <= scan_code;
                            key_press    <= 1'b1;
                            key_extended <= 1'b1;
                            key_valid    <= 1'b1;
                            state        <= NORMAL;
                        end
                    end
                    E0_F0: begin
                        key_code     <= scan_code;
                        key_press    <= 1'b0;
                        key_extended <= 1'b1;
                        key_valid    <= 1'b1;
                        state        <= NORMAL;
                    end
                endcase
            end
        end
    end

endmodule

3. FIFO缓冲区

键盘输入速度不可预测——用户可能快速连按多个键。FIFO缓冲区确保不会丢失按键:

// keyboard_fifo.v - 键盘FIFO缓冲
// 功能:16深度FIFO,缓冲按键事件防止丢失

module keyboard_fifo (
    input  wire        clk,
    input  wire        rst_n,
    // 写端口(来自扫描码解析器)
    input  wire [7:0]  wr_data,
    input  wire        wr_en,
    // 读端口(CPU读取)
    output reg [7:0]  rd_data,
    input  wire        rd_en,
    // 状态
    output reg        fifo_empty,
    output reg        fifo_full,
    output reg [4:0]  fifo_count
);

    reg [7:0] mem [0:15];  // 16×8 FIFO存储
    reg [3:0] wr_ptr;
    reg [3:0] rd_ptr;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wr_ptr     <= 4'd0;
            rd_ptr     <= 4'd0;
            fifo_count <= 5'd0;
            fifo_empty <= 1'b1;
            fifo_full  <= 1'b0;
            rd_data    <= 8'd0;
        end else begin
            // 写操作
            if (wr_en && !fifo_full) begin
                mem[wr_ptr] <= wr_data;
                wr_ptr      <= wr_ptr + 4'd1;
                fifo_count  <= fifo_count + 5'd1;
            end

            // 读操作
            if (rd_en && !fifo_empty) begin
                rd_data    <= mem[rd_ptr];
                rd_ptr     <= rd_ptr + 4'd1;
                fifo_count <= fifo_count - 5'd1;
            end

            fifo_empty <= (fifo_count == 5'd0);
            fifo_full  <= (fifo_count == 5'd16);
        end
    end

endmodule

4. 扫描码到ASCII码转换表

最终我们需要将扫描码映射为CPU可以直接处理的ASCII码:

// ps2_ascii_table.v - 扫描码到ASCII转换ROM
// 功能:将Scan Code Set 2映射为ASCII字符

module ps2_ascii_table (
    input  wire [7:0]  scan_code,
    input  wire        shift_on,    // Shift键状态
    input  wire        caps_lock,   // Caps Lock状态
    output reg [7:0]  ascii_out,
    output reg        ascii_valid  // 是否为可打印字符
);

    // Scan Code Set 2 → ASCII 查找表(简化版)
    always @(*) begin
        ascii_valid = 1'b1;
        if (!shift_on && !caps_lock) begin
            case (scan_code)
                8'h1C: ascii_out = 8'h61; // a
                8'h32: ascii_out = 8'h62; // b
                8'h21: ascii_out = 8'h63; // c
                8'h23: ascii_out = 8'h64; // d
                8'h24: ascii_out = 8'h65; // e
                8'h2B: ascii_out = 8'h66; // f
                8'h34: ascii_out = 8'h67; // g
                8'h33: ascii_out = 8'h68; // h
                8'h43: ascii_out = 8'h69; // i
                8'h3B: ascii_out = 8'h6A; // j
                8'h42: ascii_out = 8'h6B; // k
                8'h4B: ascii_out = 8'h6C; // l
                8'h3A: ascii_out = 8'h6D; // m
                8'h31: ascii_out = 8'h6E; // n
                8'h44: ascii_out = 8'h6F; // o
                8'h4D: ascii_out = 8'h70; // p
                8'h15: ascii_out = 8'h71; // q
                8'h2D: ascii_out = 8'h72; // r
                8'h1B: ascii_out = 8'h73; // s
                8'h2C: ascii_out = 8'h74; // t
                8'h3C: ascii_out = 8'h75; // u
                8'h2A: ascii_out = 8'h76; // v
                8'h1D: ascii_out = 8'h77; // w
                8'h22: ascii_out = 8'h78; // x
                8'h35: ascii_out = 8'h79; // y
                8'h1A: ascii_out = 8'h7A; // z
                8'h29: ascii_out = 8'h20; // Space
                8'h5A: ascii_out = 8'h0D; // Enter
                8'h76: ascii_out = 8'h1B; // Esc
                8'h66: ascii_out = 8'h08; // Backspace
                8'h0E: ascii_out = 8'h60; // `
                8'h4E: ascii_out = 8'h2D; // -
                8'h55: ascii_out = 8'h3D; // =
                8'h54: ascii_out = 8'h5B; // [
                8'h5B: ascii_out = 8'h5D; // ]
                8'h5D: ascii_out = 8'h5C; // \
                8'h4C: ascii_out = 8'h3B; // ;
                8'h52: ascii_out = 8'h27; // '
                8'h41: ascii_out = 8'h2C; // ,
                8'h49: ascii_out = 8'h2E; // .
                8'h4A: ascii_out = 8'h2F; // /
                default: begin
                    ascii_out   = 8'h00;
                    ascii_valid = 1'b0;
                end
            endcase
        end
        else begin
            // Shift或Caps Lock状态:大写字母和符号
            case (scan_code)
                8'h1C: ascii_out = 8'h41; // A
                8'h32: ascii_out = 8'h42; // B
                8'h21: ascii_out = 8'h43; // C
                8'h23: ascii_out = 8'h44; // D
                8'h24: ascii_out = 8'h45; // E
                8'h2B: ascii_out = 8'h46; // F
                8'h34: ascii_out = 8'h47; // G
                8'h33: ascii_out = 8'h48; // H
                8'h43: ascii_out = 8'h49; // I
                8'h3B: ascii_out = 8'h4A; // J
                8'h42: ascii_out = 8'h4B; // K
                8'h4B: ascii_out = 8'h4C; // L
                8'h3A: ascii_out = 8'h4D; // M
                8'h31: ascii_out = 8'h4E; // N
                8'h44: ascii_out = 8'h4F; // O
                8'h4D: ascii_out = 8'h50; // P
                8'h15: ascii_out = 8'h51; // Q
                8'h2D: ascii_out = 8'h52; // R
                8'h1B: ascii_out = 8'h53; // S
                8'h2C: ascii_out = 8'h54; // T
                8'h3C: ascii_out = 8'h55; // U
                8'h2A: ascii_out = 8'h56; // V
                8'h1D: ascii_out = 8'h57; // W
                8'h22: ascii_out = 8'h58; // X
                8'h35: ascii_out = 8'h59; // Y
                8'h1A: ascii_out = 8'h5A; // Z
                8'h0E: ascii_out = 8'h7E; // ~
                8'h4E: ascii_out = 8'h5F; // _
                8'h55: ascii_out = 8'h2B; // +
                8'h54: ascii_out = 8'h7B; // {
                8'h5B: ascii_out = 8'h7D; // }
                8'h5D: ascii_out = 8'h7C; // |
                8'h4C: ascii_out = 8'h3A; // :
                8'h52: ascii_out = 8'h22; // "
                8'h41: ascii_out = 8'h3C; // <
                8'h49: ascii_out = 8'h3E; // >
                8'h4A: ascii_out = 8'h3F; // ?
                default: begin
                    ascii_out   = 8'h00;
                    ascii_valid = 1'b0;
                end
            endcase
        end
    end

endmodule

🧪 Verilator仿真验证

测试平台:PS/2接收器仿真

// tb_ps2_keyboard.v - PS/2键盘接收器测试平台

module tb_ps2_keyboard;

    reg        clk;
    reg        rst_n;
    reg        ps2_clk;
    reg        ps2_data;
    wire [7:0] scan_code;
    wire       scan_ready;
    wire       scan_error;
    reg        scan_ack;

    // 实例化被测模块
    ps2_keyboard uut (
        .clk(clk),
        .rst_n(rst_n),
        .ps2_clk(ps2_clk),
        .ps2_data(ps2_data),
        .scan_code(scan_code),
        .scan_ready(scan_ready),
        .scan_error(scan_error),
        .scan_ack(scan_ack)
    );

    // 系统时钟:10ns周期 (100MHz)
    initial clk = 0;
    always #5 clk = ~clk;

    // PS/2时钟:75us周期 (~13.3KHz)
    localparam PS2_HALF = 37500; // 37.5us半周期

    // 发送一个PS/2字节的任务
    task send_ps2_byte;
        input [7:0] data;
        reg [7:0] d;
        reg       parity;
        integer   i;
        begin
            d = data;
            parity = 1'b0;
            for (i = 0; i < 8; i = i + 1)
                parity = parity ^ d[i];

            // 起始位 (0)
            ps2_data = 1'b0;
            #PS2_HALF; ps2_clk = 1'b0;
            #PS2_HALF; ps2_clk = 1'b1;

            // 数据位 (LSB first)
            for (i = 0; i < 8; i = i + 1) begin
                ps2_data = d[i];
                #PS2_HALF; ps2_clk = 1'b0;
                #PS2_HALF; ps2_clk = 1'b1;
            end

            // 奇校验位
            ps2_data = ~parity;
            #PS2_HALF; ps2_clk = 1'b0;
            #PS2_HALF; ps2_clk = 1'b1;

            // 停止位 (1)
            ps2_data = 1'b1;
            #PS2_HALF; ps2_clk = 1'b0;
            #PS2_HALF; ps2_clk = 1'b1;
        end
    endtask

    // 测试流程
    initial begin
        // 初始化
        rst_n    = 1'b0;
        ps2_clk  = 1'b1;
        ps2_data = 1'b1;
        scan_ack = 1'b0;

        #100;
        rst_n = 1'b1;
        #100;

        // 测试1:发送扫描码 0x1C (A键按下)
        $display("--- Test 1: Send 0x1C (A key) ---");
        send_ps2_byte(8'h1C);
        #10000;

        if (scan_ready && scan_code == 8'h1C)
            $display("  PASS: Received 0x1C");
        else
            $display("  FAIL: Expected 0x1C, got 0x%02h, ready=%b", scan_code, scan_ready);

        scan_ack = 1'b1;
        #100;
        scan_ack = 1'b0;

        // 测试2:发送扫描码 0xF0 (断码前缀)
        $display("--- Test 2: Send 0xF0 0x1C (A key released) ---");
        send_ps2_byte(8'hF0);
        #10000;

        if (scan_ready && scan_code == 8'hF0)
            $display("  PASS: Received F0 prefix");

        scan_ack = 1'b1;
        #100;
        scan_ack = 1'b0;

        send_ps2_byte(8'h1C);
        #10000;

        if (scan_ready && scan_code == 8'h1C)
            $display("  PASS: Received break code 0x1C");

        // 测试3:奇偶校验错误
        $display("--- Test 3: Parity error test ---");
        // 发送带错误校验位的数据(直接操纵信号)
        ps2_data = 1'b0; // 起始位
        #PS2_HALF; ps2_clk = 1'b0;
        #PS2_HALF; ps2_clk = 1'b1;
        // 发送0x55 = 01010101,正确奇校验=1,我们故意发0
        ps2_data = 1'b1; #PS2_HALF; ps2_clk = 1'b0; #PS2_HALF; ps2_clk = 1'b1;
        ps2_data = 1'b0; #PS2_HALF; ps2_clk = 1'b0; #PS2_HALF; ps2_clk = 1'b1;
        ps2_data = 1'b1; #PS2_HALF; ps2_clk = 1'b0; #PS2_HALF; ps2_clk = 1'b1;
        ps2_data = 1'b0; #PS2_HALF; ps2_clk = 1'b0; #PS2_HALF; ps2_clk = 1'b1;
        ps2_data = 1'b1; #PS2_HALF; ps2_clk = 1'b0; #PS2_HALF; ps2_clk = 1'b1;
        ps2_data = 1'b0; #PS2_HALF; ps2_clk = 1'b0; #PS2_HALF; ps2_clk = 1'b1;
        ps2_data = 1'b1; #PS2_HALF; ps2_clk = 1'b0; #PS2_HALF; ps2_clk = 1'b1;
        ps2_data = 1'b0; #PS2_HALF; ps2_clk = 1'b0; #PS2_HALF; ps2_clk = 1'b1;
        // 错误的奇校验位
        ps2_data = 1'b0; #PS2_HALF; ps2_clk = 1'b0; #PS2_HALF; ps2_clk = 1'b1;

        #10000;
        if (scan_error)
            $display("  PASS: Parity error detected");
        else
            $display("  FAIL: Parity error NOT detected");

        $display("--- All tests complete ---");
        $finish;
    end

endmodule
✅ Verilator验证通过 —— ps2_keyboard.v、ps2_scancode_parser.v、keyboard_fifo.v、ps2_ascii_table.v均通过 verilator --lint-only 检查,无错误无警告。

📋 内存映射接口

键盘控制器通过内存映射I/O与CPU通信:

地址功能读/写
$FF02[7:0]键盘数据:FIFO中最前端的ASCII码R
$FF03[0]按键就绪:1=FIFO非空R
$FF03[1]FIFO溢出:1=按键丢失R
$FF03[7:2]FIFO中待读按键数R
CPU读取键盘流程 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. 检查 $FF03 bit0 是否为1(有按键) 2. 读 $FF02 获取ASCII码(同时FIFO弹出) 3. 如需等待按键:循环检查 $FF03 示例汇编代码: WAIT_KEY: LDA $FF03 ; 读取键盘状态 AND #$01 ; 检查bit0 BEQ WAIT_KEY ; 无按键则等待 LDA $FF02 ; 读取ASCII码 RTS ; 返回,A=按键ASCII

🎯 练习

练习1:扩展键盘处理

当前实现只处理了字母键的ASCII转换。请添加数字键0-9的扫描码到ASCII映射(扫描码:0x45=0, 0x16=1, 0x1E=2, 0x26=3, 0x25=4, 0x2E=5, 0x36=6, 0x3D=7, 0x3E=8, 0x46=9),并处理Shift+数字键产生符号(!@#$%^&*())。

练习2:键盘命令发送

PS/2协议是双向的——主机也可以向键盘发送命令。实现主机到键盘的发送功能:当主机拉低时钟线超过100μs再拉低数据线时,进入"命令模式"。键盘需要响应0xFF(复位)、0xED(设置LED)、0xF3(设置重复率)等命令。

练习3:防抖处理

在真实硬件中,PS/2信号可能存在噪声。添加超时检测机制:如果接收一个字节的过程中,两个PS/2时钟沿之间的间隔超过2ms,则重置接收状态机,丢弃不完整的数据。

练习4:集成测试

编写一个完整的仿真测试:模拟用户输入"HELLO"的全过程——5个按键的通码发送,经过接收器→解析器→ASCII转换→FIFO缓冲,最终CPU端读出字符序列 0x48 0x45 0x4C 0x4C 0x4F。

🏆 成就解锁:键盘大师

你实现了从PS/2物理信号到ASCII字符的完整链路!这包括:

键盘输入是人机交互的基石——有了它,你的8位电脑终于可以"听"了!