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

550 lines
24 KiB
Verilog
Raw Permalink 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
// -----------------------------------------------------------------------------
// 上板顶层(Nexys4DDR,xc7a100tcsg324-1)
//
// 处理器顶层 cpu21_riscv_redirect_int_bpb 暴露了 775 位调试端口(各级 PC/IR、
// 计数器、BPB/中断观测等),直接作为实现顶层会因 I/O 超限而无法布局布线
// ([Place 30-415] IO Placement failed due to overutilization)。本模块只把
// 开发板真正需要的外设引出来,其余调试端口不连接,因此不占用引脚:
//
// clk100 (E3) -> CPU 时钟(100 MHz,经 CPU_DIV 分频后给 CPU;默认 50 MHz,
// 跑中断综合测试程序时用更慢的 CLK_DIV_INTRUPT,
// 让数码管上每个数值停留更久,便于观察)
// rst_n (C12) -> 复位按键 CPU_RESETN,低有效,经复位同步器
// go (N17) -> 按键 BTNC:ecall 停机后按下继续运行
// irq_sw[2:0] -> 拨码开关 SW[2:0]:3 路中断源
// hid_sel (V10) -> 拨码开关 SW[15]:选 PS/2 设备(0 = 键盘,1 = 鼠标)
// sw_flag (R15) -> 拨码开关 SW[3]:插旗模式(软件读 INPUT 寄存器)
// led[15:0] -> 板载 LED[15:0]:LED[2:0] = 三路中断指示,从该路中断
// 请求出现开始点亮,一直到它的 ISR 执行 uret 返回后才熄灭;
// LED[15:3] 熄灭
// led_we (R12) -> LED16_B:有 MMIO 写入时亮,并拉长约 0.34 s 便于观察
// an/seg/dp -> 板载 8 位七段数码管:显示程序写入 LED_ADDR 的 32 位
// 十六进制数据(每 4 位对应一个数码管)
//
// 片内存储器:
// ROM:ROM_WORDS 字,组合读,内容由 `include "cpu21_board_rom.vh"` 给出
// (该文件由 tools/hex2vh.py 从 testbench/programs/*.hex 生成);
// RAM:RAM_WORDS 字,字节写使能 + 组合读;只响应地址 < 2^(RAM_AW+2),
// 因此 MMIO 地址不会落进 RAM(避免与 LED_ADDR 别名)。
//
// 注意:ROM_WORDS / RAM_WORDS 需为 2 的幂(默认 1024,对应地址位 [11:2])。
// -----------------------------------------------------------------------------
module cpu21_board_top #(
parameter [31:0] LED_ADDR = 32'h0000_1000,
parameter ROM_WORDS = 8192,
parameter RAM_WORDS = 1024,
parameter [31:0] USTATUS_INIT = 32'h0000_0001,
// 板载 100 MHz 的分频比(1 = 直用 100 MHz,2 = 50 MHz,…)。
// 乘除法改成多周期单元后,原来 ~94 ns 的组合乘法/除法已经不在关键路径上,
// 实测(xc7a100tcsg324-1):
// CLK_DIV=1(100 MHz):WNS = -1.1 ns,违例路径是 EX 级的
// 分支判决/中断保存 → BPB 更新,与乘除法无关(逻辑只占 ~3 ns,
// 7~8 ns 都是布线);
// CLK_DIV=2( 50 MHz):WNS ≈ +8 ns,余量充足。
// 因此默认取 2,既比原来的 16 分频快 8 倍,又能保证时序收敛。
parameter integer CLK_DIV = 2,
// 中断综合测试程序(RISC_V_INTRUPT_CCAB_ROM)专用的 CPU 分频比。
// 该程序每次显示之间的延时常量是按 50 MHz 调的:50 MHz 下每个数值只停留
// 约 10 ms,数码管上根本读不出来。停留时间与分频比成正比,例如:
// 8 → 12.5 MHz(每个数值约 40 ms)、16 → 6.25 MHz(约 80 ms)、
// 32 → 3.125 MHz(约 160 ms)、64 → 1.5625 MHz(约 320 ms)。
// 取大于 2 的值只是把实际时钟变慢,XDC 的 cpu_clk 仍按最坏情况 50 MHz 约束,
// 所以不会掩盖时序违例(约束偏严)。
parameter integer CLK_DIV_INTRUPT = 32,
// PS/2 输入(板载 USB HID 口,也可改接 Pmod JA):0 = 关闭(只剩板上 5 向按键);
// 1 = 开启。开启后同时例化鼠标与键盘两个解码器(它们共用同一根 PS/2 线),
// 用 SW[15](端口 hid_sel)选哪个生效:0 = 键盘(默认),1 = 鼠标。
// 不能两个同时生效:键盘的扫描码会凑巧组成合法的鼠标包,让光标乱跳。
parameter integer USE_PS2_MOUSE = 1,
// 调试:1 = 用 LED 高位显示 PS/2 诊断信息:
// LED[15:6] = 当前选中设备收到的字节数低 10 位(按一下键/动一下鼠标就跳 = 收发都通)
// LED[5] = ps2_clk 有边沿活动(时钟在动就快速翻转)
// LED[4] = ps2_data 当前电平
// LED[3] = ps2_clk 当前电平
// LED[2:0] 始终是三路中断指示。
parameter integer DEBUG_PS2_RX_LED = 1,
// 5 向按键去抖的采样周期(CPU 周期数):上板默认 50000(1 ms @50 MHz)。
// 仿真里可以传一个小值,让"按一下"在几十微秒内就被识别。
parameter integer POINTER_SAMPLE_DIV = 50000
) (
input wire clk100, // E3
input wire rst_n, // C12:CPU_RESETN,低有效
input wire go, // N17:BTNC(同时用作鼠标左键)
input wire [ 2:0] irq_sw, // SW[2:0]:原来的 3 路中断源(保持兼容)
input wire btnu, // M18:光标上
input wire btnd, // P18:光标下
input wire btnl, // P17:光标左
input wire btnr, // M17:光标右
input wire sw_flag, // R15:SW[3],插旗模式(软件读 INPUT 寄存器)
input wire hid_sel, // V10:SW[15],选 PS/2 设备(0 = 键盘,1 = 鼠标)
output wire [15:0] led, // LED[15:0]
output wire led_we, // LED16_B:MMIO 写入指示
// 七段数码管(共阳极:位选/段选都是低有效)
output wire [ 7:0] an,
output wire [ 6:0] seg,
output wire dp,
// VGA
output wire [ 3:0] vga_r,
output wire [ 3:0] vga_g,
output wire [ 3:0] vga_b,
output wire vga_hs,
output wire vga_vs,
// PS/2(只有 USE_PS2_MOUSE=1 时才用)
input wire ps2_clk,
input wire ps2_data
);
localparam ROM_AW = $clog2(ROM_WORDS);
localparam RAM_AW = $clog2(RAM_WORDS);
// CPU 时钟实际用的分频比:中断综合测试程序用更慢的 CLK_DIV_INTRUPT,
// 其余程序用 CLK_DIV。两者必须都是偶数(保证 50% 占空比)。
`ifdef RISC_V_INTRUPT_CCAB_ROM
localparam integer CPU_DIV = CLK_DIV_INTRUPT;
`elsif RISC_V_INTRUPT_SLLSB_ROM
localparam integer CPU_DIV = CLK_DIV_INTRUPT;
`else
localparam integer CPU_DIV = CLK_DIV;
`endif
// 数码管扫描分频:目标是“每位数码管约 125 Hz 刷新”(即每步 1 ms),与 CPU
// 时钟频率无关。每步时间 = REFRESH_DIV / f_cpu,而 f_cpu = 100 MHz / CPU_DIV,
// 所以 REFRESH_DIV = 1 ms × f_cpu = 100000 / CPU_DIV:
// CPU_DIV = 2( 50 MHz)-> 50000(每步 1 ms,每位数码管 125 Hz)
// CPU_DIV = 8(12.5 MHz)-> 12500(同上)
// 例:CPU_DIV = 32(3.125 MHz)-> 3125(每步仍是 1 ms)。
// 不这样缩放的话,CPU 降频时扫描会按同样比例变慢,肉眼能看到闪烁。
localparam integer SEG_REFRESH_DIV = 100000 / CPU_DIV;
wire arst = ~rst_n; // 异步复位(高有效)
// ---------------------------------------------------------------------------
// 时钟:100 MHz / CPU_DIV 给 CPU。默认 50 MHz(CLK_DIV=2);跑中断综合测试
// 程序时用更慢的 CLK_DIV_INTRUPT(见该参数说明),让数码管上的数值停留更久。
//
// 乘除法已经是 cpu21_riscv_muldiv 里的多周期单元,关键路径上不再有组合的
// 32x32 乘法/除法;剩下的违例路径是 EX 级“分支判决/中断保存 → BPB 更新”,
// 与乘除法无关,实测 100 MHz 下 WNS = -1.1 ns,50 MHz 下余量约 +8 ns。
// 分频后的时钟由 Vivado 自动插 BUFG;XDC 里用 create_generated_clock 声明。
// XDC 按最坏情况固定成 -divide_by 2(50 MHz),所以这里把实际时钟变慢属于
// “约束偏严”,不会掩盖时序违例。
//
// 中断综合测试程序会用更慢的 CPU_DIV(见 CLK_DIV_INTRUPT):程序里的延时常量
// 按 50 MHz 调好,50 MHz 下每个数值只停留约 10 ms,读数根本看不清;
// 停留时间与分频比成正比,具体见 CLK_DIV_INTRUPT 的说明。
// 注意:VGA 的 25 MHz 像素节拍也是从这个 CPU 时钟分频出来的,CPU 降频时
// VGA 行场时序会跟着变慢(超出 VGA 规范)。中断测试程序不用 VGA,无影响。
// ---------------------------------------------------------------------------
reg [15:0] clk_cnt;
reg clk_div;
always @(posedge clk100 or posedge arst) begin
if (arst) begin
clk_cnt <= 16'd0;
clk_div <= 1'b0;
end else if (clk_cnt == (CPU_DIV / 2 - 1)) begin
clk_cnt <= 16'd0;
clk_div <= ~clk_div;
end else begin
clk_cnt <= clk_cnt + 16'd1;
end
end
wire clk_cpu = clk_div;
// ---------------------------------------------------------------------------
// 复位同步器:按键异步置位、释放与 CPU 时钟同步
// ---------------------------------------------------------------------------
reg [2:0] rst_sync_q;
always @(posedge clk_cpu or posedge arst) begin
if (arst) rst_sync_q <= 3'b111;
else rst_sync_q <= {rst_sync_q[1:0], 1'b0};
end
wire reset = rst_sync_q[2];
// CPU 接口连线(先声明:后面的 ROM/RAM 逻辑与例化都要用到)
wire [31:0] instr_addr;
wire [31:0] data_addr;
wire [31:0] data_wdata;
wire [ 3:0] data_wstrb;
wire data_we;
wire [15:0] ps2_rx_count; // PS/2 鼠标收到的字节数(用于 LED 调试显示)
wire [15:0] kbd_rx_count; // PS/2 键盘收到的字节数(先声明:LED 调试段要用)
wire [31:0] led_data; // LED_ADDR 写入值,同时作为七段数码管显示数据
wire led_valid;
// 扫雷扩展外设的连线(先声明,例化在下面)
wire [31:0] disp_ctrl, disp_cursor; // 显示控制器(CPU 内部 MMIO 驱动)
wire [31:0] seg_data; // 保留的独立数码管 MMIO 数据
wire seg_valid;
wire fb_we; // 显存写端口
wire [ 9:0] fb_waddr;
wire [31:0] fb_wdata;
wire [ 3:0] fb_wstrb;
wire [ 9:0] fb_raddr; // 显存读端口(VGA)
wire mouse_irq, level_irq; // 鼠标事件中断 / 软件中断(关卡入口)
wire [1:0] irq_level; // 当前正在服务的中断级别(0 = 不在中断里,1/2/3 = 在 ISR 里)
wire frame_tick;
wire [15:0] sw_vec = {12'b0, sw_flag, irq_sw};
// 中断连线:SW0 与鼠标共用一个入口,SW1 与"关卡入口"共用一个入口。
// 中断线对应关系(必须与 saolei/sw/linker.ld、mmio.h 的约定一致):
// irq_vec[0] = IRQ1 -> 入口 0x100 = 关卡(游戏),由 LEVEL 软件中断拉起
// irq_vec[1] = IRQ2 -> 入口 0x200 = 鼠标读取,由事件 FIFO 非空拉起
// irq_vec[2] = IRQ3 -> 入口 0x300 = 备用
// (原来 mouse 接在 [0]、level 接在 [1],正好对调:第一个鼠标事件会跳进关卡
// 入口把棋盘画出来,而鼠标 ISR 永不执行,FIFO 不弹空、中断线恒高 —— 于是
// 只能收到一次事件。拨码开关 SW[2:0] 仍按位 OR 进来,兼容原中断测试程序。)
wire [2:0] irq_vec = {irq_sw[2], (mouse_irq | irq_sw[1]), (level_irq | irq_sw[0])};
// ---------------------------------------------------------------------------
// 指令 ROM:组合读(未用到的地址填 NOP)
// ---------------------------------------------------------------------------
reg [31:0] rom[0:ROM_WORDS-1];
integer i;
initial begin
for (i = 0; i < ROM_WORDS; i = i + 1) rom[i] = 32'h0000_0013; // NOP 填充
`ifdef SAOLEI_ROM
// 扫雷程序(由 saolei/tools/build.ps1 生成的 cpu21_saolei_rom.vh)。
// 综合/仿真时定义宏 SAOLEI_ROM 即可切换到这个程序;不定义就是下面的跑马灯。
`include "cpu21_saolei_rom.vh"
`elsif RISC_V_INTRUPT_CCAB_ROM
// 中断综合测试程序:三个中断入口由镜像中的实际标签位置决定。
`include "risc-v-intrupt-ccab.vh"
`elsif RISC_V_INTRUPT_SLLSB_ROM
// 课程新版中断测试(附加 SLL/SB/BLTU),ISR 入口 0x0AC / 0x240 / 0x3E8。
`include "risc-v-intrupt-sllsb.vh"
`elsif BOARD_ROM_FAST
// 仿真专用:把演示程序的延时改小的版本(综合流程不会用到)
`include "cpu21_board_rom_fast.vh"
`else
`include "cpu21_board_rom.vh"
`endif
end
// ---------------------------------------------------------------------------
// 数据 RAM:字节写使能 + 组合读
// ---------------------------------------------------------------------------
reg [31:0] ram[0:RAM_WORDS-1];
integer j;
initial begin
for (j = 0; j < RAM_WORDS; j = j + 1) ram[j] = 32'b0;
end
wire [ROM_AW+1:2] rom_index = instr_addr[ROM_AW+1:2];
wire [RAM_AW+1:2] ram_index = data_addr[RAM_AW+1:2];
wire ram_sel = (data_addr[31:RAM_AW+2] == 0);
wire [31:0] instr_word = rom[rom_index];
wire [31:0] data_rdata = ram_sel ? ram[ram_index] : 32'b0;
always @(posedge clk_cpu) begin
if (ram_sel && data_we) begin
if (data_wstrb[0]) ram[ram_index][7:0] <= data_wdata[7:0];
if (data_wstrb[1]) ram[ram_index][15:8] <= data_wdata[15:8];
if (data_wstrb[2]) ram[ram_index][23:16] <= data_wdata[23:16];
if (data_wstrb[3]) ram[ram_index][31:24] <= data_wdata[31:24];
end
end
// ---------------------------------------------------------------------------
// 处理器:只连接上板需要的端口,调试端口留空(不产生 I/O)
// ---------------------------------------------------------------------------
cpu21_riscv_redirect_int_bpb #(
.LED_ADDR (LED_ADDR),
.USTATUS_INIT(USTATUS_INIT),
// 中断入口必须与所选 ROM 镜像中的 ISR 起点一致。
`ifdef RISC_V_INTRUPT_CCAB_ROM
// risc-v-intrupt-ccab.hex:字 43/121/194
.IRQ1_VECTOR (32'h0000_00AC),
.IRQ2_VECTOR (32'h0000_01E4),
.IRQ3_VECTOR (32'h0000_0308)
`elsif RISC_V_INTRUPT_SLLSB_ROM
// risc-v-intrupt-sllsb.hex:字 43/144/250(加了 SLL/SB/BLTU 后 ISR2/3 后移)
.IRQ1_VECTOR (32'h0000_00AC),
.IRQ2_VECTOR (32'h0000_0240),
.IRQ3_VECTOR (32'h0000_03E8)
`else
// 扫雷程序:linker.ld 中固定的 0x100/0x200/0x300
.IRQ1_VECTOR (32'h0000_0100),
.IRQ2_VECTOR (32'h0000_0200),
.IRQ3_VECTOR (32'h0000_0300)
`endif
) u_cpu (
.clk (clk_cpu),
.reset (reset),
.instr_i (instr_word),
.data_rdata_i(data_rdata),
.irq_i (irq_vec),
.go_i (go),
.instr_addr_o(instr_addr),
.data_addr_o (data_addr),
.data_wdata_o(data_wdata),
.data_wstrb_o(data_wstrb),
.data_we_o (data_we),
.led_data_o (led_data),
.led_valid_o(led_valid),
// 扫雷扩展外设
.seg_data_o (seg_data),
.seg_valid_o (seg_valid),
.fb_we_o (fb_we),
.fb_waddr_o (fb_waddr),
.fb_wdata_o (fb_wdata),
.fb_wstrb_o (fb_wstrb),
.fb_rdata_i (fb_rdata),
.disp_ctrl_o (disp_ctrl),
.disp_cursor_o (disp_cursor),
.mouse_ev_valid_i (ev_valid),
.mouse_dx_i (ev_dx),
.mouse_dy_i (ev_dy),
.mouse_btn_i (ev_btn),
.sw_i (sw_vec),
.frame_tick_i (frame_tick),
.mouse_irq_o (mouse_irq),
.level_irq_o (level_irq),
// 中断服务级别,供板上的中断指示 LED 使用(见下面 LED 那一段)
.irq_current_level_o(irq_level)
);
// ---------------------------------------------------------------------------
// 板载 LED:三路中断指示;十六进制数据改由七段数码管显示。
//
// 第 k 路中断从"请求出现"开始点亮,一直亮到它的 ISR 执行完 uret(中断返回)
// 之后才熄灭:
// 置位:irq_vec[k] 的上升沿。中断控制器内部也是按上升沿响应中断的
// (cpu21_riscv_irq_ctrl 里的 irq_event),两者行为一致;开关一直
// 接通不会重复点亮,只有新的上升沿才会重新置位。
// 清零:服务级别 irq_level 从 k+1 **降下来**,说明第 k+1 级的中断已经返回。
// 注意必须判断“下降”:级别从 1 升到 2 是深层中断抢占,此时第 1 级
// 的中断还在服务中,LED 不能熄。中断可以嵌套,返回时按级别逐层降
// 下来,因此每一路都在自己的 ISR 结束时熄灭。
// ---------------------------------------------------------------------------
reg [2:0] irq_req_q; // 中断请求打一拍,用于取上升沿
reg [1:0] irq_level_q; // 服务级别打一拍,用于检测"返回"
reg [2:0] irq_led_q; // 三路中断指示
wire isr_ret0 = (irq_level_q == 2'd1) && (irq_level < irq_level_q);
wire isr_ret1 = (irq_level_q == 2'd2) && (irq_level < irq_level_q);
wire isr_ret2 = (irq_level_q == 2'd3) && (irq_level < irq_level_q);
always @(posedge clk_cpu or posedge arst) begin
if (arst) begin
irq_req_q <= 3'b000;
irq_level_q <= 2'd0;
irq_led_q <= 3'b000;
end else begin
irq_req_q <= irq_vec;
irq_level_q <= irq_level;
// 请求上升沿 -> 点亮;对应级别的 ISR 返回 -> 熄灭
irq_led_q[0] <= (irq_vec[0] & ~irq_req_q[0]) | (irq_led_q[0] & ~isr_ret0);
irq_led_q[1] <= (irq_vec[1] & ~irq_req_q[1]) | (irq_led_q[1] & ~isr_ret1);
irq_led_q[2] <= (irq_vec[2] & ~irq_req_q[2]) | (irq_led_q[2] & ~isr_ret2);
end
end
// ---- PS/2 输入诊断:把两根线的电平与活动情况摆到 LED 上,
// 便于判断"板载 USB HID 芯片到底有没有在发 PS/2 数据" ----
reg ps2_clk_s1, ps2_clk_s2, ps2_data_s1, ps2_data_s2;
reg [7:0] ps2_edge_cnt;
always @(posedge clk_cpu 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;
ps2_edge_cnt <= 8'd0;
end else begin
ps2_clk_s1 <= ps2_clk;
ps2_clk_s2 <= ps2_clk_s1;
ps2_data_s1 <= ps2_data;
ps2_data_s2 <= ps2_data_s1;
if (ps2_clk_s1 != ps2_clk_s2)
ps2_edge_cnt <= ps2_edge_cnt + 8'd1;
end
end
// 当前选中的 PS/2 设备收到的字节数(SW[15] 选谁就显示谁)
wire [15:0] hid_rx_sel = hid_sel ? ps2_rx_count : kbd_rx_count;
// LED[15:3] 平时熄灭;DEBUG_PS2_RX_LED=1 时显示 PS/2 诊断信息(含义见参数注释)
assign led = (DEBUG_PS2_RX_LED != 0)
? {hid_rx_sel[9:0], ps2_edge_cnt[0], ps2_data_s2, ps2_clk_s2, irq_led_q}
: {{13{1'b0}}, irq_led_q};
reg [23:0] led_we_cnt;
always @(posedge clk_cpu or posedge arst) begin
if (arst) led_we_cnt <= 24'd0;
else if (led_valid) led_we_cnt <= 24'hFF_FFFF; // 约 0.34 s @50 MHz
// (12.5 MHz 下约 1.36 s)
else if (led_we_cnt != 24'd0) led_we_cnt <= led_we_cnt - 24'd1;
end
assign led_we = (led_we_cnt != 24'd0);
// ===========================================================================
// 扫雷外设:显存 + VGA + 七段数码管 + 鼠标事件源 + 中断连线
//
// 显存是一块 2400 字节的双口 RAM(40x30 格,每格 2 字节):CPU 通过 MMIO 的
// fb_* 写端口写入,VGA 用另一个读端口组合读出。两者同处 clk_cpu 域(VGA 的
// 25 MHz 像素节拍由使能信号产生,不是独立时钟),因此没有跨时钟域问题。
// ===========================================================================
localparam integer FB_WORDS = 600; // 600 字 = 2400 字节,40x30 格每格 2 字节
// ---- 显存 ----
reg [31:0] fb[0:FB_WORDS-1];
integer k;
initial begin
for (k = 0; k < FB_WORDS; k = k + 1) fb[k] = 32'b0;
end
always @(posedge clk_cpu) begin
if (fb_we) begin
if (fb_wstrb[0]) fb[fb_waddr][7:0] <= fb_wdata[7:0];
if (fb_wstrb[1]) fb[fb_waddr][15:8] <= fb_wdata[15:8];
if (fb_wstrb[2]) fb[fb_waddr][23:16] <= fb_wdata[23:16];
if (fb_wstrb[3]) fb[fb_waddr][31:24] <= fb_wdata[31:24];
end
end
wire [31:0] fb_rdata = fb[fb_raddr];
// ---- VGA 像素节拍:clk_cpu 二分频 => 25 MHz ----
reg px_q;
always @(posedge clk_cpu or posedge arst) begin
if (arst) px_q <= 1'b0;
else px_q <= ~px_q;
end
wire px_tick = ~px_q;
cpu21_vga u_vga (
.clk (clk_cpu),
.reset (reset),
.tick (px_tick),
.fb_raddr_o (fb_raddr),
.fb_word_i (fb_rdata),
.ctrl_i (disp_ctrl),
.cursor_i (disp_cursor),
.vga_r (vga_r),
.vga_g (vga_g),
.vga_b (vga_b),
.vga_hs (vga_hs),
.vga_vs (vga_vs),
.frame_tick_o(frame_tick)
);
// ---- 七段数码管(显示程序写入 LED_ADDR 的 8 位十六进制数)----
// 扫描分频按 CPU 时钟频率缩放,保持每位数码管约 125 Hz 刷新(不闪烁)。
cpu21_seg7 #(
.REFRESH_DIV(SEG_REFRESH_DIV)
) u_seg7 (
.clk (clk_cpu),
.reset (reset),
.seg_data_i(led_data),
.an_en_i (8'hFF),
.an_o (an),
.seg_o (seg),
.dp_o (dp)
);
// ---- 鼠标事件源:板载按键 与 PS/2 鼠标**同时**有效(两路事件在顶层合并) ----
wire ev_valid;
wire [7:0] ev_dx, ev_dy;
wire [4:0] ev_btn;
wire [4:0] btn_vec = {go, btnu, btnd, btnl, btnr}; // {BTNC, BTNU, BTND, BTNL, BTNR}
// 1) 板载按键:永远保留(BTNC 当左键、上下左右当移动、SW3 当插旗开关)
wire ptr_valid;
wire [7:0] ptr_dx, ptr_dy;
wire [4:0] ptr_btn;
cpu21_pointer #(
.SAMPLE_DIV(POINTER_SAMPLE_DIV)
) u_ptr (
.clk (clk_cpu),
.reset (reset),
.btn_i (btn_vec),
.ev_valid_o(ptr_valid),
.ev_dx_o (ptr_dx),
.ev_dy_o (ptr_dy),
.ev_btn_o (ptr_btn)
);
// 2) PS/2 鼠标:板载 USB HID 口经板上芯片转成 PS/2 信号(ps2_clk/ps2_data)
wire ps2_valid;
wire [7:0] ps2_dx, ps2_dy;
wire [4:0] ps2_btn;
// PS/2 键盘:与鼠标共用同一根 PS/2 线(板上只有一个 USB HID 口)。
// 线上只有一个设备在说话,"是哪一个"用 SW[15] 告诉硬件(hid_sel)。
wire kbd_valid;
wire [7:0] kbd_dx, kbd_dy;
wire [4:0] kbd_btn;
generate
if (USE_PS2_MOUSE != 0) begin : g_ps2
cpu21_ps2_mouse u_ps2 (
.clk (clk_cpu),
.reset (reset),
.ps2_clk_i (ps2_clk),
.ps2_clk_oe_o (),
.ps2_data_i (ps2_data),
.ps2_data_oe_o(),
.ev_valid_o (ps2_valid),
.ev_dx_o (ps2_dx),
.ev_dy_o (ps2_dy),
.ev_btn_o (ps2_btn),
.rx_count_o (ps2_rx_count)
);
cpu21_ps2_kbd u_kbd (
.clk (clk_cpu),
.reset (reset),
.ps2_clk_i (ps2_clk),
.ps2_data_i (ps2_data),
.ev_valid_o (kbd_valid),
.ev_dx_o (kbd_dx),
.ev_dy_o (kbd_dy),
.ev_btn_o (kbd_btn),
.rx_count_o (kbd_rx_count)
);
end else begin : g_no_ps2
assign ps2_valid = 1'b0;
assign ps2_dx = 8'd0;
assign ps2_dy = 8'd0;
assign ps2_btn = 5'd0;
assign ps2_rx_count = 16'd0;
assign kbd_valid = 1'b0;
assign kbd_dx = 8'd0;
assign kbd_dy = 8'd0;
assign kbd_btn = 5'd0;
assign kbd_rx_count = 16'd0;
end
endgenerate
// 3) 只放行 SW[15] 选中的那一路 PS/2 设备(另一个解码器虽然也在听,但事件被挡住)
wire m_valid = ps2_valid & hid_sel; // SW[15] = 1 -> PS/2 鼠标
wire k_valid = kbd_valid & ~hid_sel; // SW[15] = 0 -> PS/2 键盘
// 4) 合并:三个源相互独立,同一拍只会有一方产生事件;优先 PS/2
assign ev_valid = m_valid | k_valid | ptr_valid;
assign ev_dx = m_valid ? ps2_dx : k_valid ? kbd_dx : ptr_dx;
assign ev_dy = m_valid ? ps2_dy : k_valid ? kbd_dy : ptr_dy;
assign ev_btn = m_valid ? ps2_btn : k_valid ? kbd_btn : ptr_btn;
endmodule
`default_nettype wire