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 路径
This commit is contained in:
@@ -38,6 +38,7 @@ module tb_board_top;
|
||||
.btnl (1'b0),
|
||||
.btnr (1'b0),
|
||||
.sw_flag (1'b0),
|
||||
.hid_sel (1'b0),
|
||||
.ps2_clk (1'b1),
|
||||
.ps2_data(1'b1),
|
||||
.an (),
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
`timescale 1ns / 1ps
|
||||
// =============================================================================
|
||||
// tb_ps2_kbd —— 单独验证 PS/2 键盘解码(扫描码集 2)
|
||||
//
|
||||
// 发 → 键(E0 74)应产生 dx=+8;发 ↑(E0 75)应产生 dy=-8;
|
||||
// 发 Enter(5A)应产生 btn=1(开格);发 左Shift(12)应产生 btn=2(插旗);
|
||||
// 松开码(F0 xx)不应产生事件;校验位错的帧应被整帧丢掉。
|
||||
//
|
||||
// PS/2 键盘发扫描码(集 2):0xF0 前缀 = 下一个码是"松开",0xE0 前缀 = 扩展键,
|
||||
// 方向键是扩展键(↑ = E0 75、↓ = E0 72、← = E0 6B、→ = E0 74),
|
||||
// 所以解码器只对"按下"产生事件。键盘自动重复 = 按住方向键时连续移动。
|
||||
//
|
||||
// 跑法:
|
||||
// xvlog srcs/rtl/cpu21_ps2_kbd.v srcs/tb/tb_ps2_kbd.v
|
||||
// xelab -top tb_ps2_kbd -snapshot kbdsim
|
||||
// xsim kbdsim -R
|
||||
// =============================================================================
|
||||
module tb_ps2_kbd;
|
||||
|
||||
reg clk = 1'b0;
|
||||
reg reset = 1'b1;
|
||||
reg ps2_clk = 1'b1;
|
||||
reg ps2_data = 1'b1;
|
||||
|
||||
wire ev_valid;
|
||||
wire [7:0] ev_dx, ev_dy;
|
||||
wire [4:0] ev_btn;
|
||||
wire [15:0] rx_count;
|
||||
|
||||
always #10 clk = ~clk; // 50 MHz
|
||||
|
||||
cpu21_ps2_kbd dut (
|
||||
.clk (clk),
|
||||
.reset (reset),
|
||||
.ps2_clk_i (ps2_clk),
|
||||
.ps2_data_i (ps2_data),
|
||||
.ev_valid_o (ev_valid),
|
||||
.ev_dx_o (ev_dx),
|
||||
.ev_dy_o (ev_dy),
|
||||
.ev_btn_o (ev_btn),
|
||||
.rx_count_o (rx_count)
|
||||
);
|
||||
|
||||
// 发一个 PS/2 位:先摆数据,稳定后拉低时钟(DUT 在下降沿采样)
|
||||
task send_bit;
|
||||
input b;
|
||||
begin
|
||||
ps2_data = b;
|
||||
#(10_000);
|
||||
ps2_clk = 1'b0;
|
||||
#(20_000);
|
||||
ps2_clk = 1'b1;
|
||||
#(20_000);
|
||||
end
|
||||
endtask
|
||||
|
||||
// 发一个字节:起始 0 + 8 位数据(LSB first) + 奇校验 + 停止 1
|
||||
task send_byte;
|
||||
input [7:0] d;
|
||||
integer i;
|
||||
reg p;
|
||||
begin
|
||||
p = ~^d; // 奇校验
|
||||
send_bit(1'b0);
|
||||
for (i = 0; i < 8; i = i + 1)
|
||||
send_bit(d[i]);
|
||||
send_bit(p);
|
||||
send_bit(1'b1);
|
||||
end
|
||||
endtask
|
||||
|
||||
// 校验位错的字节(应被整帧丢掉)
|
||||
task send_byte_bad_parity;
|
||||
input [7:0] d;
|
||||
integer i;
|
||||
begin
|
||||
send_bit(1'b0);
|
||||
for (i = 0; i < 8; i = i + 1)
|
||||
send_bit(d[i]);
|
||||
send_bit(^d);
|
||||
send_bit(1'b1);
|
||||
end
|
||||
endtask
|
||||
|
||||
integer n_ev = 0;
|
||||
integer ok = 1;
|
||||
integer exp_dx, exp_dy, exp_btn;
|
||||
|
||||
always @(posedge clk) begin
|
||||
if (ev_valid) begin
|
||||
n_ev = n_ev + 1;
|
||||
$display(" [kbd] event #%0d: dx=%0d dy=%0d btn=%b", n_ev, $signed(ev_dx), $signed(ev_dy), ev_btn);
|
||||
if ((exp_dx != 999) && ($signed(ev_dx) != exp_dx)) begin
|
||||
$display(" [kbd] FAIL: 期望 dx=%0d", exp_dx);
|
||||
ok = 0;
|
||||
end
|
||||
if ((exp_dy != 999) && ($signed(ev_dy) != exp_dy)) begin
|
||||
$display(" [kbd] FAIL: 期望 dy=%0d", exp_dy);
|
||||
ok = 0;
|
||||
end
|
||||
if ((exp_btn != 999) && (ev_btn != exp_btn)) begin
|
||||
$display(" [kbd] FAIL: 期望 btn=%b", exp_btn);
|
||||
ok = 0;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
initial begin
|
||||
$display("[kbd] === PS/2 keyboard decode test ===");
|
||||
#2000;
|
||||
reset = 1'b0;
|
||||
#2000;
|
||||
|
||||
// 校验错的字节(0x74)不能被当成"→"键
|
||||
$display("[kbd] 校验位错的字节应该被丢掉");
|
||||
exp_dx = 999; exp_dy = 999; exp_btn = 999;
|
||||
send_byte_bad_parity(8'h74);
|
||||
#(100_000);
|
||||
if (n_ev != 0) begin
|
||||
$display(" [kbd] FAIL: 校验错的帧不该产生事件");
|
||||
ok = 0;
|
||||
end
|
||||
|
||||
$display("[kbd] 只发松开码 F0 75(不该有事件)");
|
||||
send_byte(8'hF0);
|
||||
send_byte(8'h75);
|
||||
#(100_000);
|
||||
if (n_ev != 0) begin
|
||||
$display(" [kbd] FAIL: 松开码不该产生事件");
|
||||
ok = 0;
|
||||
end
|
||||
|
||||
$display("[kbd] → 键(E0 74)期望 dx=+8");
|
||||
exp_dx = 8; exp_dy = 0; exp_btn = 0;
|
||||
send_byte(8'hE0);
|
||||
send_byte(8'h74);
|
||||
#(100_000);
|
||||
|
||||
$display("[kbd] ↑ 键(E0 75)期望 dy=-8");
|
||||
exp_dx = 0; exp_dy = -8; exp_btn = 0;
|
||||
send_byte(8'hE0);
|
||||
send_byte(8'h75);
|
||||
#(100_000);
|
||||
|
||||
$display("[kbd] Enter(5A)期望 btn=00001");
|
||||
exp_dx = 0; exp_dy = 0; exp_btn = 5'b00001;
|
||||
send_byte(8'h5A);
|
||||
#(100_000);
|
||||
|
||||
$display("[kbd] 左 Shift(12)期望 btn=00010(软件里 = 插旗)");
|
||||
exp_dx = 0; exp_dy = 0; exp_btn = 5'b00010;
|
||||
send_byte(8'h12);
|
||||
#(100_000);
|
||||
|
||||
$display("[kbd] rx_count=%0d events=%0d", rx_count, n_ev);
|
||||
if ((ok == 0) || (n_ev != 4))
|
||||
$display("[kbd] === FAIL ===");
|
||||
else
|
||||
$display("[kbd] === ALL PASS ===");
|
||||
$finish;
|
||||
end
|
||||
|
||||
endmodule
|
||||
@@ -0,0 +1,148 @@
|
||||
`timescale 1ns / 1ps
|
||||
// =============================================================================
|
||||
// tb_ps2_mouse —— 单独验证 PS/2 鼠标接收逻辑(不跑 CPU,几秒出结果)
|
||||
//
|
||||
// 模拟 PS/2 鼠标发 3 字节包:
|
||||
// byte0 = 标志字节(bit3 必须为 1 才是有效的鼠标数据;bit0/1/2 = 左/右/中键)
|
||||
// byte1 = dx(有符号) byte2 = dy(有符号)
|
||||
// 检查 cpu21_ps2_mouse 是否给出正确的 ev_valid / ev_dx / ev_dy / ev_btn。
|
||||
//
|
||||
// PS/2 时序:DUT 在 ps2_clk 的**下降沿**采样 ps2_data,所以这里先把数据摆好,
|
||||
// 再拉低时钟。一帧 11 位 = 起始 0 + 8 位数据(LSB 先) + **奇校验** + 停止 1。
|
||||
// 奇校验 = 校验位让 data + parity 共 9 位里 1 的个数为奇数(真设备就是这么发的)。
|
||||
//
|
||||
// 跑法(Vivado 的 xvlog/xelab/xsim 或其它仿真器都行):
|
||||
// xvlog srcs/rtl/cpu21_ps2_mouse.v srcs/tb/tb_ps2_mouse.v
|
||||
// xelab -top tb_ps2_mouse -snapshot ps2sim
|
||||
// xsim ps2sim -R
|
||||
// =============================================================================
|
||||
module tb_ps2_mouse;
|
||||
|
||||
reg clk = 1'b0;
|
||||
reg reset = 1'b1;
|
||||
reg ps2_clk = 1'b1;
|
||||
reg ps2_data = 1'b1;
|
||||
|
||||
wire ev_valid;
|
||||
wire [7:0] ev_dx;
|
||||
wire [7:0] ev_dy;
|
||||
wire [4:0] ev_btn;
|
||||
wire [15:0] rx_count;
|
||||
|
||||
always #10 clk = ~clk; // 50 MHz,和板上 CPU 主频一致
|
||||
|
||||
cpu21_ps2_mouse dut (
|
||||
.clk (clk),
|
||||
.reset (reset),
|
||||
.ps2_clk_i (ps2_clk),
|
||||
.ps2_clk_oe_o (),
|
||||
.ps2_data_i (ps2_data),
|
||||
.ps2_data_oe_o(),
|
||||
.ev_valid_o (ev_valid),
|
||||
.ev_dx_o (ev_dx),
|
||||
.ev_dy_o (ev_dy),
|
||||
.ev_btn_o (ev_btn),
|
||||
.rx_count_o (rx_count)
|
||||
);
|
||||
|
||||
// 发一个 PS/2 位:先摆数据,稳定后再拉低时钟(DUT 在下降沿采样)
|
||||
task send_bit;
|
||||
input b;
|
||||
begin
|
||||
ps2_data = b;
|
||||
#(10_000);
|
||||
ps2_clk = 1'b0;
|
||||
#(20_000);
|
||||
ps2_clk = 1'b1;
|
||||
#(20_000);
|
||||
end
|
||||
endtask
|
||||
|
||||
// 发一个字节:起始 0 + 8 位数据(LSB first) + 奇校验 + 停止 1
|
||||
task send_byte;
|
||||
input [7:0] d;
|
||||
integer i;
|
||||
reg p;
|
||||
begin
|
||||
p = ~^d; // 奇校验:data + parity 里 1 的个数为奇数
|
||||
send_bit(1'b0);
|
||||
for (i = 0; i < 8; i = i + 1)
|
||||
send_bit(d[i]);
|
||||
send_bit(p);
|
||||
send_bit(1'b1);
|
||||
end
|
||||
endtask
|
||||
|
||||
// 故意发一个校验位错的字节(用于确认校验真的在起作用)
|
||||
task send_byte_bad_parity;
|
||||
input [7:0] d;
|
||||
integer i;
|
||||
begin
|
||||
send_bit(1'b0);
|
||||
for (i = 0; i < 8; i = i + 1)
|
||||
send_bit(d[i]);
|
||||
send_bit(^d); // 错误的校验位
|
||||
send_bit(1'b1);
|
||||
end
|
||||
endtask
|
||||
|
||||
integer n_ev = 0;
|
||||
integer ok = 1;
|
||||
|
||||
always @(posedge clk) begin
|
||||
if (ev_valid) begin
|
||||
n_ev = n_ev + 1;
|
||||
$display(" [ps2] event #%0d: dx=%0d dy=%0d btn=%b", n_ev, $signed(ev_dx), $signed(ev_dy), ev_btn);
|
||||
if (n_ev == 1) begin
|
||||
if (($signed(ev_dx) != 8) || ($signed(ev_dy) != -8) || (ev_btn != 5'b00000)) begin
|
||||
$display(" [ps2] FAIL: 第 1 包应为 dx=+8 dy=-8 btn=00000");
|
||||
ok = 0;
|
||||
end
|
||||
end
|
||||
if (n_ev == 2) begin
|
||||
if (($signed(ev_dx) != -3) || ($signed(ev_dy) != 5) || (ev_btn[0] != 1'b1)) begin
|
||||
$display(" [ps2] FAIL: 第 2 包应为 dx=-3 dy=+5 btn=xxxx1");
|
||||
ok = 0;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
integer rx_after_bad;
|
||||
|
||||
initial begin
|
||||
$display("[ps2] === PS/2 mouse receiver test ===");
|
||||
#2000;
|
||||
reset = 1'b0;
|
||||
#2000;
|
||||
|
||||
$display("[ps2] 校验位错的字节应该被丢掉");
|
||||
send_byte_bad_parity(8'h08);
|
||||
#(100_000);
|
||||
if (rx_count != 16'd0) begin
|
||||
$display(" [ps2] FAIL: 校验错的帧不该被计数(rx_count=%0d)", rx_count);
|
||||
ok = 0;
|
||||
end
|
||||
rx_after_bad = rx_count;
|
||||
|
||||
$display("[ps2] packet 1: flag=0x08(no button) dx=+8 dy=-8");
|
||||
send_byte(8'h08);
|
||||
send_byte(8'h08);
|
||||
send_byte(8'hF8);
|
||||
#(100_000);
|
||||
|
||||
$display("[ps2] packet 2: flag=0x09(left) dx=-3 dy=+5");
|
||||
send_byte(8'h09);
|
||||
send_byte(8'hFD);
|
||||
send_byte(8'h05);
|
||||
#(100_000);
|
||||
|
||||
$display("[ps2] rx_count=%0d events=%0d", rx_count, n_ev);
|
||||
if ((rx_count != rx_after_bad + 16'd6) || (n_ev != 2) || (ok == 0))
|
||||
$display("[ps2] === FAIL ===");
|
||||
else
|
||||
$display("[ps2] === ALL PASS ===");
|
||||
$finish;
|
||||
end
|
||||
|
||||
endmodule
|
||||
Reference in New Issue
Block a user