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:
@@ -0,0 +1,142 @@
|
||||
`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
|
||||
Reference in New Issue
Block a user