Files
risc-v-cpu/pipelined-cpu/srcs/rtl/cpu21_ps2_kbd.v
Alpha 28c696ece2 fix(ps2): 鼠标 dy 方向取负(PS/2 正 dy = 上,软件正 dy = 下);键盘补 W/A/S/D 别名
上板实测:鼠标已能操作(说明“奇校验 + 帧按位的下标对齐”那个修复就是根因),
但上下是反的、左右正确 —— 这是两套约定的差异,不是解码错:
  * PS/2 鼠标原生约定:dx 正 = 右、**dy 正 = 上**
  * saolei.c 的约定:正 dx = 列 +1、**正 dy = 行 +1(向下)**,和 cpu21_pointer 的
    BTND = +8 一致
所以 cpu21_ps2_mouse 在拼包时把 dy 取负(dx 不动),三种事件源(板载按键 / PS/2 鼠标 /
PS/2 键盘)方向统一。

键盘:另加 W/A/S/D = 方向键(与 tools/play_saolei.tcl 里的别名保持一致),这样用普通
字母键也能测。

自测同步更新(都 ALL PASS):
  * tb_ps2_mouse:改发“鼠标向上”的 dy=+8,事件里必须得到 dy=-8;向下 dy=-5 -> +5
  * tb_ps2_kbd:新增 W(1D) -> dy=-8 用例
2026-09-23 22:50:19 +08:00

152 lines
7.2 KiB
Verilog
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
`timescale 1ns / 1ps
// =============================================================================
// cpu21_ps2_kbd —— PS/2 键盘接收(扫描码集 2),把按键映射成"鼠标事件"
//
// 为什么映射成鼠标事件:saolei.c / isr.c 早就是按鼠标事件 {btn, dy, dx} 写的,
// 键盘只要产出**同样格式**的事件,软件一行都不用改。
//
// ↑ ↓ ← → 光标移动(dx/dy = ±8,恰好是软件里的"一格")
// W A S D 同上(别名,与 tools/play_saolei.tcl 一致)
// Enter / 空格 左键(开格)
// 左 Shift / F / C 右键(软件里右键 = 插旗)
//
// 方向约定跟软件对齐:**dy = +8 是向下**(与 cpu21_pointer 的 BTND、cpu21_ps2_mouse 一致),
// 所以 ↑ 发 dy = 248(= -8 的补码)。
//
// 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
// W/A/S/D 也当方向键(和 tools/play_saolei.tcl 里的别名保持一致)
8'h1D: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd248; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // W = ↑
8'h1B: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd8; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // S = ↓
8'h1C: begin ev_dx_o <= 8'd248; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // A = ←
8'h23: begin ev_dx_o <= 8'd8; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // D = →
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