Files
risc-v-cpu/pipelined-cpu/srcs/rtl/cpu21_ps2_kbd.v
T
Alpha 9dfff16794 feat(board): 支持 PS/2 键盘(与鼠标共用板载 USB HID 口),并修好 PS/2 帧校验
一、修 PS/2 接收器的真 bug —— 这就是上板“鼠标毫无反应”的根因
  鼠标和键盘两个接收模块原来是同一个写法错误:
  * 帧用整体右移 {data, frame_q[10:1]},判 frame_q[10] 时停止位还没移进来,判到的
    其实是校验位;取数据 frame_q[8:1] 也因此整体错一位
  * 校验位按“偶校验”判,而 PS/2 是“奇校验”
  两者叠加让真设备发来的帧 100% 被丢掉(上板表现为 rx_count 恒为 0,看起来像硬件
  或跳线问题)。现在按“位的下标”存帧(frame_q[i] = 第 i 位,所以字节 = frame_q[8:1]、
  校验 = frame_q[9]),停止位直接在当前拍的 ps2_data 上判,校验按奇校验。

二、新增 PS/2 键盘解码 cpu21_ps2_kbd.v
  扫描码集 2:E0 = 扩展前缀、F0 = 松开前缀,只对“按下”产生事件,并映射成与鼠标
  完全相同的事件格式(↑↓←→ = dy/dx ±8,正好一格;Enter/空格 = 左键;左 Shift/F/C
  = 右键 = 插旗),所以 saolei 的 C 代码一行都不用改;按住方向键 = 连续移动(用的是
  键盘自己的重复码)。

三、顶层 cpu21_board_top:用 SW15 选设备
  同一根 PS/2 线上只会插一个设备,但两个解码器都在听:键盘的扫描码会凑巧组成
  “合法的鼠标包”,两路同时生效会让光标乱跳。因此新增端口 hid_sel(SW15):
  0 = 键盘(默认),1 = 鼠标;诊断 LED 改显示“当前选中设备”收到的字节数。

四、其它
  * XDC:hid_sel -> V10(SW15),引脚号在官方母版里核对过
  * tb_board_top 补 hid_sel 端口,避免悬空
  * 新增自测 srcs/tb/tb_ps2_mouse.v / tb_ps2_kbd.v:含“故意发一个校验位错的帧,
    必须丢帧”的用例,两个都 ALL PASS
  * README / saolei/README:新增“PS/2 键鼠”一节(接线方式、SW15、LED 诊断口诀、
    键位表),修正显存格式(每格 2 字节、0x2000-0x2FFF)、扫雷难度与验证状态、
    修好 riscv 工具链 -Prefix 路径
2026-09-23 22:35:50 +08:00

143 lines
6.3 KiB
Verilog
Raw 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,恰好是软件里的"一格")
// 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