一、修 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 路径
156 lines
7.3 KiB
Verilog
156 lines
7.3 KiB
Verilog
`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
|