Files
risc-v-cpu/pipelined-cpu/srcs/rtl/cpu21_ps2_mouse.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

156 lines
7.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
`default_nettype none
// -----------------------------------------------------------------------------
// cpu21_ps2_mouse —— 可选的真鼠标输入:PS/2 鼠标(Digilent Pmod PS2 插在 JA 上)。
//
// JA[1] = PS/2 DATA,JA[2] = PS/2 CLK(两条线都需要上拉)。当前 XDC 把 ps2_clk /
// ps2_data 接到板载 USB HID 口的 PS/2 侧(F4 / B2),也可以改回 JA(C17 / D18)。
//
// 注意:接收/解析逻辑(包括帧对齐与奇校验)已由 srcs/tb/tb_ps2_mouse.v 仿真验证;
// 板上那一侧能不能收到数据,取决于板载 USB→PS/2 桥接芯片是否肯转发你插的设备。
// 如果你不用真鼠标,请让顶层用 cpu21_pointer(5 向按键)作为事件源即可,
// C 程序完全不用改。板级顶层 cpu21_board_top 用参数 USE_PS2_MOUSE 选择这两者。
//
// 实现内容:
// 1) 接收:在 ps2_clk 的下降沿采样 ps2_data,收满 11 位(起始 0 + 8 数据
// LSB 先 + 奇校验 + 停止 1)后校验并拼成 3 字节一包;
// frame_q[i] = 这一帧的第 i 位(0 起始、1..8 = d0..d7、9 校验、10 停止),
// 所以字节 = frame_q[8:1](frame_q[1] 就是 d0),校验 = frame_q[9]。
// 注意 PS/2 是**奇校验**:data + parity 9 位里 1 的个数为奇数。
// 2) 解析:byte0 = 标志(bit0 左键、bit1 右键、bit3 固定 1),byte1 = dx,
// byte2 = dy,bit7 为符号位;
// 3) 上报:当 dx/dy 非 0 或按键状态变化时产生一个事件(单位与
// cpu21_pointer 相同,即原始计数 = 1/8 格);
// 4) 本模块只做"接收",不实现主机→鼠标的命令发送(0xF4 使能上报)。多数
// PS/2 鼠标上电后就会主动上报,这类鼠标可以直接用;如果你的鼠标上电毫无
// 反应,说明它需要主机先发 0xF4 —— 那部分需要自行补上,或者直接用默认的
// 5 向按键(cpu21_pointer),C 程序完全不用改。
// -----------------------------------------------------------------------------
module cpu21_ps2_mouse (
input wire clk,
input wire reset,
// PS/2 双向线(开漏)
input wire ps2_clk_i,
output wire ps2_clk_oe_o,
input wire ps2_data_i,
output wire ps2_data_oe_o,
// 事件上报
output reg ev_valid_o,
output reg [7:0] ev_dx_o,
output reg [7:0] ev_dy_o,
output reg [4:0] ev_btn_o,
// 调试:收到的字节数(可选观察)
output reg [15:0] rx_count_o
);
// 开漏:oe = 1 时把线拉低,否则高阻(外部上拉)。本模块只接收,两条线都不驱动。
assign ps2_clk_oe_o = 1'b0;
assign ps2_data_oe_o = 1'b0;
// 两级同步
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; // 同步后的下降沿
wire clk_rise = ps2_clk_s1 & ~ps2_clk_s2;
// ---------------------------------------------------------------------------
// 接收移位:11 位一帧,bit0 = 起始位,bit1..8 = 数据,bit9 = 校验,bit10 = 停止
// ---------------------------------------------------------------------------
reg [3:0] bit_cnt_q;
reg [10:0] frame_q;
reg frame_ok_q;
reg [23:0] pkt_q; // 3 字节包
reg [1:0] pkt_cnt_q;
reg [7:0] byte_cnt_q; // 一包内的字节序号(0..2)
reg [7:0] dx_q, dy_q;
reg [4:0] btn_q; // {4'b0, middle, right, left} -> 只取低 3 位使用
// 校验:PS/2 是**奇校验**,data 8 位 + parity 位共 9 位里 1 的个数应为奇数
function automatic parity_ok(input [8:0] dp); // dp = {parity, d7..d0}
parity_ok = ^dp;
endfunction
always @(posedge clk or posedge reset) begin
if (reset) begin
bit_cnt_q <= 4'd0;
frame_q <= 11'd0;
frame_ok_q <= 1'b0;
byte_cnt_q <= 8'd0;
pkt_q <= 24'd0;
rx_count_o <= 16'd0;
ev_valid_o <= 1'b0;
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])) begin
rx_count_o <= rx_count_o + 16'd1;
case (byte_cnt_q)
8'd0: begin
// 标志字节:bit3 必须为 1(否则是鼠标发来的其他响应,丢弃)
// 数据字节 = frame_q[8:1],它的 bit3 就是 frame_q[4]
// (不能写 frame_q[8:1][3]:Verilog-2001 不允许 part-select 再取位)
if (frame_q[4] == 1'b1) begin
pkt_q[7:0] <= frame_q[8:1];
byte_cnt_q <= 8'd1;
end
end
8'd1: begin
pkt_q[15:8] <= frame_q[8:1]; // dx
byte_cnt_q <= 8'd2;
end
8'd2: begin
byte_cnt_q <= 8'd0;
ev_dx_o <= pkt_q[15:8]; // byte1 = dx
ev_dy_o <= frame_q[8:1]; // byte2 = dy
// 与 cpu21_mmio / mmio.h 一致:bit0=左键, bit1=右键, bit2=中键
ev_btn_o <= {2'b0, pkt_q[2], pkt_q[1], pkt_q[0]};
ev_valid_o <= 1'b1;
end
default:
byte_cnt_q <= 8'd0;
endcase
end
end
else begin
bit_cnt_q <= bit_cnt_q + 4'd1;
end
end
end
end
end
endmodule
`default_nettype wire