Files
risc-v-cpu/pipelined-cpu/srcs/tb/tb_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

164 lines
5.0 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
// =============================================================================
// 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