`timescale 1ns / 1ps // ============================================================================= // cpu21_ps2_kbd —— PS/2 键盘接收(扫描码集 2),把按键映射成"鼠标事件" // // 为什么映射成鼠标事件:saolei.c / isr.c 早就是按鼠标事件 {btn, dy, dx} 写的, // 键盘只要产出**同样格式**的事件,软件一行都不用改。 // // ↑ ↓ ← → 光标移动(dx/dy = ±8,恰好是软件里的"一格") // Enter / 空格 左键(开格) // 左 Shift / F / C 右键(软件里右键 = 插旗) // // PS/2 键盘发扫描码(集 2): // 0xF0 前缀 -> 下一个码是"松开"(break);0xE0 前缀 -> 下一个码是扩展键 // 方向键是扩展键:↑ = E0 75、↓ = E0 72、← = E0 6B、→ = E0 74 // 所以只对"按下"(make)产生事件。按住不动时键盘自己会发重复码, // 于是按住方向键正好变成"连续移动"。 // // 与 cpu21_ps2_mouse 共用同一根 PS/2 线:两个接收器都挂在线上,只有插着的那个 // 设备会产生有效数据,所以顶层把两者的事件一起 OR 起来即可,不用二选一。 // ============================================================================= module cpu21_ps2_kbd ( input wire clk, input wire reset, input wire ps2_clk_i, input wire ps2_data_i, output reg ev_valid_o, // 单周期脉冲 output reg [7:0] ev_dx_o, // 有符号,1/8 格(±8 = 一格) output reg [7:0] ev_dy_o, output reg [4:0] ev_btn_o, // bit0 = 左键, bit1 = 右键 output reg [15:0] rx_count_o // 收到的字节数(调试观察用) ); // ---- 两级同步 ---- reg ps2_clk_s1, ps2_clk_s2, ps2_data_s1, ps2_data_s2; always @(posedge clk or posedge reset) begin if (reset) begin ps2_clk_s1 <= 1'b1; ps2_clk_s2 <= 1'b1; ps2_data_s1 <= 1'b1; ps2_data_s2 <= 1'b1; end else begin ps2_clk_s1 <= ps2_clk_i; ps2_clk_s2 <= ps2_clk_s1; ps2_data_s1 <= ps2_data_i; ps2_data_s2 <= ps2_data_s1; end end wire clk_fall = ~ps2_clk_s1 & ps2_clk_s2; reg [3:0] bit_cnt_q; reg [10:0] frame_q; reg brk_q; // 刚收到 0xF0:下一个码是松开 reg ext_q; // 刚收到 0xE0:下一个码是扩展键 // 11 位里 data + parity 共 9 位,PS/2 是**奇校验**:1 的个数应为奇数 function automatic parity_ok(input [8:0] dp); // dp = {parity, d7..d0} parity_ok = ^dp; endfunction // 处理一个扫描码(按下才产生事件) task handle_code; input [7:0] sc; reg pressed; begin rx_count_o <= rx_count_o + 16'd1; pressed = (brk_q == 1'b0); // 0xF0 之后的码是"松开" brk_q <= 1'b0; if (sc == 8'hE0) ext_q <= 1'b1; // 扩展前缀 else if (sc == 8'hF0) brk_q <= 1'b1; // 断码前缀 else if (pressed) begin if (ext_q) begin case (sc) 8'h75: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd248; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // ↑ 8'h72: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd8; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // ↓ 8'h6B: begin ev_dx_o <= 8'd248; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // ← 8'h74: begin ev_dx_o <= 8'd8; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // → default: ; endcase end else begin case (sc) 8'h5A: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00001; ev_valid_o <= 1'b1; end // Enter 8'h29: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00001; ev_valid_o <= 1'b1; end // 空格 8'h12: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00010; ev_valid_o <= 1'b1; end // 左 Shift 8'h2B: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00010; ev_valid_o <= 1'b1; end // F 8'h2E: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00010; ev_valid_o <= 1'b1; end // C default: ; endcase end end // 一次前缀组合处理完,清掉扩展标志(松开码 E0 F0 xx 也走这里) if ((sc != 8'hE0) && (sc != 8'hF0)) ext_q <= 1'b0; end endtask always @(posedge clk or posedge reset) begin if (reset) begin bit_cnt_q <= 4'd0; frame_q <= 11'd0; brk_q <= 1'b0; ext_q <= 1'b0; ev_valid_o <= 1'b0; ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; rx_count_o <= 16'd0; end else begin ev_valid_o <= 1'b0; if (clk_fall) begin // frame_q[i] = 这一帧的第 i 位:0 = 起始,1..8 = 数据 d0..d7(LSB 先), // 9 = 校验,10 = 停止。按”位的下标“存,不要再整体右移: // 右移的话停止位还没进来就先去判 frame_q[10],判到的是校验位。 if (bit_cnt_q == 4'd0) begin if (ps2_data_s2 == 1'b0) // 起始位有效,开始收 bit_cnt_q <= 4'd1; end else begin frame_q[bit_cnt_q] <= ps2_data_s2; if (bit_cnt_q == 4'd10) begin bit_cnt_q <= 4'd0; // 停止位就是刚采到的这一位(frame_q[10] 本拍还没更新到), // 校验位查 data + parity 的奇校验 if ((ps2_data_s2 == 1'b1) && parity_ok(frame_q[9:1])) handle_code(frame_q[8:1]); end else bit_cnt_q <= bit_cnt_q + 4'd1; end end end end endmodule