⌨️ 第17课:键盘缓冲

📖 本课目标

设计并实现键盘缓冲区(FIFO队列),解决键盘输入与CPU处理速度不匹配的问题。键盘可能产生比CPU处理更快的按键事件,FIFO缓冲区确保不丢失任何按键。

🧠 为什么需要键盘缓冲区?

键盘是异步输入设备——用户随时可能按键。如果CPU正在忙于其他任务,新按键就会丢失。解决方案是用一个FIFO(First In, First Out)缓冲区暂存按键,CPU可以在方便时读取。

键盘FIFO缓冲区结构 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 键盘扫描码 ──▶ [FIFO] ──▶ CPU读取($FF02) 16字节 FIFO状态: ┌───┬───┬───┬───┬───┬───┬───┬───┐ │ 41│ 42│ 1C│ A0│ │ │ │ │ ... (16字节) └───┴───┴───┴───┴───┴───┴───┴───┘ ▲ ▲ head tail (读指针) (写指针) head = tail: 缓冲区空 head = tail+1: 缓冲区满(留1个空位区分) count = (tail - head) mod 16 操作: 写入(FIFO未满时): buf[tail] = data; tail = (tail+1) % 16 读取(FIFO未空时): data = buf[head]; head = (head+1) % 16

🔧 Verilog实现

// ========================================
// kbd_fifo.v - 键盘FIFO缓冲区
// Retro8 复古电脑 输入子系统
// ✅Verilator验证通过
// ========================================

module kbd_fifo (
    input  wire          clk,
    input  wire          rst_n,
    // 写入侧(键盘)
    input  wire [7:0]   kbd_scancode,
    input  wire          kbd_valid,
    // 读取侧(CPU)
    input  wire          cpu_rd,
    output reg  [7:0]   cpu_data,
    // 状态
    output wire          fifo_empty,
    output wire          fifo_full,
    output wire          fifo_overflow
);

    localparam FIFO_DEPTH = 16;
    localparam FIFO_WIDTH = 8;

    // FIFO存储
    reg [FIFO_WIDTH-1:0] fifo_mem [0:FIFO_DEPTH-1];

    // 读写指针
    reg [3:0] head;
    reg [3:0] tail;
    reg [4:0] count;  // 5位,可以表示0-16

    // 状态输出
    assign fifo_empty = (count == 5'd0);
    assign fifo_full  = (count == 5'd16);

    // 溢出检测
    reg overflow_flag;
    assign fifo_overflow = overflow_flag;

    // 写入逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            head <= 4'd0;
            tail <= 4'd0;
            count <= 5'd0;
            overflow_flag <= 1'b0;
        end else begin
            // 写入
            if (kbd_valid && !fifo_full) begin
                fifo_mem[tail] <= kbd_scancode;
                tail <= tail + 4'd1;
                count <= count + 5'd1;
            end else if (kbd_valid && fifo_full) begin
                overflow_flag <= 1'b1;
            end

            // 读取
            if (cpu_rd && !fifo_empty) begin
                head <= head + 4'd1;
                count <= count - 5'd1;
                overflow_flag <= 1'b0;  // 读取后清除溢出标志
            end
        end
    end

    // 读出数据
    always @(*) begin
        if (!fifo_empty)
            cpu_data = fifo_mem[head];
        else
            cpu_data = 8'hFF;  // 空时返回0xFF
    end

endmodule

🔍 FIFO设计要点

环形缓冲区vs线性缓冲区

我们使用环形缓冲区(circular buffer)实现FIFO。读写指针到达数组末尾后自动回绕到开头。这比线性缓冲区更高效——不需要搬移数据。

关键问题:如何区分"满"和"空"?当head==tail时,可能是满也可能是空。我们用独立的计数器(count)来区分,这比"留一个空位"的方法更直观。

📝 练习

练习1:可配置深度的FIFO

修改FIFO模块,通过参数配置深度(4/8/16/32字节)。考虑:不同深度如何影响地址指针的位宽?

练习2:键盘去抖动

物理按键会产生抖动(短时间内多次通断)。在FIFO写入前添加去抖动逻辑:连续采样N次相同的扫描码才认为是有效按键。

练习3:FIFO状态监控

添加FIFO使用率监控:当FIFO使用超过75%时产生中断,提醒CPU尽快读取。这种"水位线"中断在嵌入式系统中很常见。

🏆 成就解锁

⌨️ 输入守护者

达成条件:

奖励:用户输入再也不会丢失!键盘缓冲区是人机交互的基础。

🔬 键盘扫描码详解

PS/2扫描码集

PS/2键盘使用扫描码集2(Scan Code Set 2),这是最广泛使用的协议:

按键通码(Make)断码(Break)
A1CF0,1C
B32F0,32
Enter5AF0,5A
Shift(左)12F0,12
CapsLock58F0,58
Esc76F0,76
空格29F0,29

扩展键(如方向键)使用E0前缀:E0,xx(通码)和E0,F0,xx(断码)。

扫描码到ASCII的转换

从键盘收到的扫描码需要转换为ASCII字符。这需要:

  1. Shift状态跟踪:记录Shift/CapsLock是否按下
  2. 查表转换:根据Shift状态选择不同的转换表
  3. 特殊键处理:功能键、方向键等不产生ASCII
// 扫描码转ASCII查表
reg [7:0] scancode_to_ascii [0:127];
reg [7:0] scancode_to_ascii_shift [0:127];

initial begin
    // 小写字母
    scancode_to_ascii[8'h1C] = 8'h61;  // a
    scancode_to_ascii[8'h32] = 8'h62;  // b
    // ...
    
    // Shift+字母 = 大写
    scancode_to_ascii_shift[8'h1C] = 8'h41;  // A
    scancode_to_ascii_shift[8'h32] = 8'h42;  // B
end

FIFO的硬件优化

我们的16字节FIFO可以用FPGA的块RAM实现,但更高效的方法是使用移位寄存器(SRL)原语:

📐 键盘驱动设计

完整的键盘驱动需要处理多种按键事件和修饰键状态:

键盘状态机

// 键盘驱动状态机
// 处理PS/2扫描码序列

IDLE:
    收到扫描码 → 检查是否扩展键前缀(E0)
    E0 → EXTENDED状态
    F0 → BREAK状态
    其他 → MAKE事件(按键按下)

EXTENDED:
    收到扫描码 → 检查是否断码
    F0 → EXT_BREAK状态
    其他 → 扩展键MAKE事件

BREAK:
    收到扫描码 → 普通键BREAK事件(按键释放)

EXT_BREAK:
    收到扫描码 → 扩展键BREAK事件

修饰键状态跟踪

Shift、Ctrl、Alt、CapsLock等修饰键需要特殊处理:

// 修饰键状态字节
// bit0: Left Shift
// bit1: Right Shift  
// bit2: Left Ctrl
// bit3: Right Ctrl
// bit4: Left Alt
// bit5: CapsLock
// bit6: NumLock
// bit7: ScrollLock

reg [7:0] modifier_state;

always @(posedge clk) begin
    if (make_code == 8'h12) modifier_state[0] <= 1;  // LShift press
    if (break_code == 8'h12) modifier_state[0] <= 0;  // LShift release
    if (make_code == 8'h59) modifier_state[1] <= 1;  // RShift press
    if (break_code == 8'h59) modifier_state[1] <= 0;  // RShift release
    if (make_code == 8'h14) modifier_state[2] <= 1;  // LCtrl press
    if (break_code == 8'h14) modifier_state[2] <= 0;  // LCtrl release
    // CapsLock是切换键:每次按下翻转状态
    if (make_code == 8'h58) modifier_state[5] <= ~modifier_state[5];
end

// Shift影响字母大小写和数字符号
wire shift_active = modifier_state[0] || modifier_state[1];
wire caps_active = modifier_state[5];
wire uppercase = shift_active ^ caps_active;  // XOR: 只有一个生效时大写

键盘缓冲区溢出策略

当缓冲区满时有几种处理方式:

我们的实现采用丢弃最新策略(overflow_flag置1但不覆盖),这是最安全的方式。