设计并实现键盘缓冲区(FIFO队列),解决键盘输入与CPU处理速度不匹配的问题。键盘可能产生比CPU处理更快的按键事件,FIFO缓冲区确保不丢失任何按键。
键盘是异步输入设备——用户随时可能按键。如果CPU正在忙于其他任务,新按键就会丢失。解决方案是用一个FIFO(First In, First Out)缓冲区暂存按键,CPU可以在方便时读取。
// ========================================
// 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
我们使用环形缓冲区(circular buffer)实现FIFO。读写指针到达数组末尾后自动回绕到开头。这比线性缓冲区更高效——不需要搬移数据。
关键问题:如何区分"满"和"空"?当head==tail时,可能是满也可能是空。我们用独立的计数器(count)来区分,这比"留一个空位"的方法更直观。
修改FIFO模块,通过参数配置深度(4/8/16/32字节)。考虑:不同深度如何影响地址指针的位宽?
物理按键会产生抖动(短时间内多次通断)。在FIFO写入前添加去抖动逻辑:连续采样N次相同的扫描码才认为是有效按键。
添加FIFO使用率监控:当FIFO使用超过75%时产生中断,提醒CPU尽快读取。这种"水位线"中断在嵌入式系统中很常见。
达成条件:
奖励:用户输入再也不会丢失!键盘缓冲区是人机交互的基础。
PS/2键盘使用扫描码集2(Scan Code Set 2),这是最广泛使用的协议:
| 按键 | 通码(Make) | 断码(Break) |
|---|---|---|
| A | 1C | F0,1C |
| B | 32 | F0,32 |
| Enter | 5A | F0,5A |
| Shift(左) | 12 | F0,12 |
| CapsLock | 58 | F0,58 |
| Esc | 76 | F0,76 |
| 空格 | 29 | F0,29 |
扩展键(如方向键)使用E0前缀:E0,xx(通码)和E0,F0,xx(断码)。
从键盘收到的扫描码需要转换为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
我们的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但不覆盖),这是最安全的方式。