一、修 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 路径
扫雷(RISC-V / cpu21)— 纯 C + MMIO 显示 + 中断读鼠标
给 pipelined-cpu 里那颗 CPU(cpu21_riscv_redirect_int_bpb)写的扫雷。
游戏逻辑和三个中断入口都由纯 C 写成,用 RISC-V 工具链编译成机器码后固化进片内 ROM 运行。
saolei/
├─ sw/
│ ├─ mmio.h 全部 MMIO 地址与格子编码(换外设地址只改这一个文件)
│ ├─ saolei.c 纯 C 扫雷:背景界面(main) + 关卡(level_main) + 棋盘与绘制
│ ├─ isr.c **中断读鼠标的代码**:三个固定地址入口 + 纯 C 服务主体 isr_body
│ ├─ start.c C 启动(设 sp、清 .bss、进 main)——只有这一处必需的内联汇编
│ └─ linker.ld ROM/RAM 布局:复位 0x0、中断入口 0x100/0x200/0x300、栈顶 0x0FFC
└─ tools/
├─ build.ps1 一键构建:C → .o → ELF → .hex + cpu21_saolei_rom.vh
├─ check_isa.py 逐条反汇编检查:有没有用到这颗核没实现的指令
└─ elf2hex.py 裸二进制 → $readmemh hex + ROM 初始化头文件
配套硬件在 Vivado 工程里(不在本目录):srcs/rtl/cpu21_mmio.v(外设中枢)、
cpu21_vga.v(显示)、cpu21_seg7.v(数码管)、cpu21_pointer.v(按键当鼠标)、
cpu21_ps2_mouse.v / cpu21_ps2_kbd.v(可选真鼠标/真键盘)、
srcs/board/cpu21_board_top.v(上板顶层)。
1. 程序结构:背景"伪操作系统" + 关卡 + 鼠标,正好占三个中断
CPU 复位后从 0x0 开始跑 main()(背景程序,从头到尾一直运行),它画开始界面
并等鼠标。整套交互是这样串起来的:
sequenceDiagram
participant BG as 背景 main()(0x0 起,一直跑)
participant LV as 关卡 level_main()(IRQ1 = 0x100)
participant MS as 鼠标 ISR(IRQ2 = 0x200)
BG->>BG: 画开始界面(标题 + 绿色"开始"按钮 + 光标框)
BG->>BG: 点中"开始"按钮 → 写 LEVEL 寄存器 = 1
Note over BG: 硬件拉起 irq_i[1],CPU 跳到 0x100
BG->>LV: isr1_entry:保存现场 + 打开 MIE → level_main()
LV->>LV: 初始化关卡(布雷、画盘面)
MS-->>LV: 鼠标事件中断(IRQ2 优先级更高,嵌套进来)
LV->>LV: 轮询事件队列:移动光标 / 揭开 / 插旗
LV->>BG: 点左上角黄格 = 返回 → 清 LEVEL + uret
Note over BG: 回到被打断的那条指令,继续等下一次"开始"
| 入口 | 地址 | 作用 | 对应 RTL 参数 |
|---|---|---|---|
| 复位 | 0x000 |
_start:设栈、进 main |
RESET_PC |
| IRQ1 | 0x100 |
关卡(游戏)程序 level_main() |
IRQ1_VECTOR |
| IRQ2 | 0x200 |
鼠标读取 isr_body() |
IRQ2_VECTOR |
| IRQ3 | 0x300 |
备用(只 uret) |
IRQ3_VECTOR |
地址是链接脚本定死的(linker.ld 里 .text.isr1 0x100 : {...}),所以先定地址、
再设 RTL 参数,两边成对;不存在"编完才知道地址"的问题。优先级 IRQ3 > IRQ2 > IRQ1,
所以"关卡跑在 IRQ1、鼠标跑在 IRQ2"天然允许鼠标嵌套打断关卡。
中断硬件行为(见 cpu21_riscv_irq_ctrl.v):irq_i 上升沿置挂起位;响应时硬件自动清
挂起位、清 ustatus[0](MIE)、把返回地址压进内部 EPC 栈;返回用 uret;硬件不保存
任何通用寄存器。因此:
isr1_entry(游戏体量大会调函数)保存全部调用者保存寄存器,并用csrrsi x0,0x004,1打开 MIE 让鼠标中断能嵌套;isr2_entry只存ra,C 主体isr_body用-ffixed-x1,x5..x7,x10..x17,x28..x31编译,逼 GCC 只用s0-s11(callee-saved,函数自己会存/恢复)和sp—— 这样纯 C 主体不会破坏被打断的代码;- 事件流:鼠标/按键 → RTL 事件 FIFO(深 16)→ 中断 →
isr_body搬进软件环形队列 → 主循环handle_event()处理。FIFO 读空后中断线自动回 0,下一个事件重新产生上升沿, 所以不丢事件;主循环只能轮询(这颗核没有wfi)。
2. 针对这颗 CPU 的硬约束(改 C 代码时别踩)
cpu21_riscv_decoder.v 实现的是 RV32I 的一个子集:
| 类别 | 有 | 没有 |
|---|---|---|
| R 型 | add sub sll slt sltu xor srl sra or and | — |
| M 扩展 | mul mulh mulhsu mulhu divu remu | div / rem(有符号) |
| I 型 | addi slti sltiu xori ori andi slli srli srai | — |
| 访存 | lw / sw / sb | lb / lh / lbu / lhu / sh |
| 高位 | lui | auipc |
| 控制流 | 6 条分支 + jal + jalr | — |
| 系统 | ecall uret csrrw csrrsi csrrci | mret / sret / fence / ebreak / wfi |
对应做法:不按字节读内存(写显存用 sb 可以)、不做有符号除法、索引运算全用
2 的幂 + 移位/掩码、不能有 .data/.rodata(数据空间读不到 ROM,所以没有带初值的
全局变量、没有字符串/常量表);字模放在 RTL 里,不在软件里。
3. 构建(已实测通过)
工具链:xPack riscv-none-elf-gcc(本机装在
C:\Users\29653\tools\riscv\xpack-riscv-none-elf-gcc-15.2.0-1\bin,已加入用户 PATH)。
cd <仓库>\saolei
powershell -ExecutionPolicy Bypass -File tools\build.ps1
# 若没配 PATH:-Prefix 'C:\Users\29653\tools\riscv\xpack-riscv-none-elf-gcc-15.2.0-1\bin\riscv-none-elf-'
| 步骤 | 说明 |
|---|---|
| 编译 | start.c/saolei.c 正常编;isr.c 额外加 -ffixed-x... |
| 链接 | -T sw/linker.ld;脚本里的 ASSERT 会拦住 .data/.rodata |
| 反汇编 | build/saolei.lst ← 这就是"编译出来的汇编",可以直接读 |
| ISA 检查 | tools/check_isa.py 逐条比对指令白名单,出现 auipc/lb/lbu/div… 直接报错并给修复提示 |
| 转格式 | build/saolei.hex($readmemh)+ pipelined-cpu/srcs/board/cpu21_saolei_rom.vh |
当前实测结果:text=3368 B, data=0, bss=1436 B,842 条指令全部通过 ISA 检查,
入口地址 _start=0x0、isr1_entry=0x100、isr2_entry=0x200、isr3_entry=0x300。
编译参数里有三处是这颗核/工具链的坑,已经写在脚本注释里:
-march=rv32im_zicsr—— GCC 15 起rv32im不含 Zicsr,csrrsi会报unrecognized opcode ... extension 'zicsr' required;- 不要禁链接器松弛(别加
-mno-relax/-Wl,--no-relax)—— gcc 的函数调用默认是auipc ra,%pcrel_hi+jalr,要靠链接器松弛压成一条jal;禁掉后 35 条auipc会留在代码里(这颗核没有)。用-msmall-data-limit=0保证松弛不会引入 gp 相对寻址; - 中断入口地址必须写在段声明上(
.text.isr1 0x100 : {...} > ROM)—— 只写. = 0x100;会被ld忽略(实测 isr1 掉到了 0x14)。
4. MMIO 地址表(与 cpu21_mmio.v 的译码一致)
| 地址 | 外设 |
|---|---|
0x0000_1000 |
LED 数据寄存器(低 16 位点亮板载 LED) |
0x0000_1004 |
七段数码管:8 位十六进制数(低 4 位 = 最右) |
0x0000_1100+ |
鼠标:STATUS / DATA / ACK / CTRL / INPUT / TIME / FRAME / EVCNT / LEVEL / MAGIC |
0x0000_1140+ |
显示控制器:CTRL(光标可见、背景色)/ CURSOR(列、行)/ MAGIC |
0x0000_2000-0x2FFF |
显存:40 列 × 30 行、每格 2 字节(共 2400 字节) |
LEVEL 是软件中断寄存器:写 1 拉起 irq_i[1](关卡入口),关卡返回前写 0 清掉,
否则下次点"开始"没有新的上升沿。MAGIC(0xCAFE0001/2)可以读来自检 MMIO 是否活着。
格子编码为两个字节:第一个字节低 6 位是字形编号,第二个字节低 3 位是颜色;字形 0 空白、 1..8 数字、9 未揭开、10 旗、11 雷、12 爆炸、13 问号、14 横线、15 实心、16..41 为 A-Z; 颜色是 3 位调色板。
5. 怎么跑起来
仿真(用你现成的测试平台):
copy build\saolei.hex <仓库>\pipelined-cpu\srcs\rom\saolei.hex
# 把 srcs/tb/tb_no_intr.v 顶部的 ROM_FILE 改成 "saolei.hex"
上板:在 Vivado 里给综合/仿真定义宏 SAOLEI_ROM(Settings → Verilog Macros),
cpu21_board_top.v 就会加载 cpu21_saolei_rom.vh;不定义仍是原来的跑马灯程序。
接 VGA 显示器,按 CPU_RESETN 复位 → 出现开始界面(标题 + 绿色按钮 + 闪动光标框)。
操作(板载 5 向按键一直有效):
| 操作 | 板载按键 | PS/2 设备(SW15 选) |
|---|---|---|
| 移动光标 | BTNU / BTND / BTNL / BTNR |
键盘 ↑↓←→(按住 = 连续)/ 鼠标移动 |
| 左键(揭开;开始界面 = 选难度/开始) | BTNC |
键盘 Enter/空格 / 鼠标左键 |
| 右键(插旗 / 取消旗) | SW3(或 SW0)拨上 + BTNC |
键盘左 Shift/F/C / 鼠标右键 |
| 返回上一层(关卡内) | 光标移到盘面左上角黄格再按左键 | 同上 |
PS/2 键鼠(板载 USB HOST 口,也可把 Pmod PS/2 插在 JA):顶层参数
USE_PS2_MOUSE 默认为 1,鼠标解码器和键盘解码器都挂在同一根 PS/2 线上,用 SW15
选哪个生效(0 = 键盘,1 = 鼠标)。键盘解的是扫描码集 2(E0 扩展前缀、F0 松开
前缀,只对按下产生事件),映射成与鼠标完全一样的事件格式,所以 C 代码一行不用改。
接收器只做接收(不实现主机发 0xF4 使能上报):多数键鼠上电即上报。上板时用
LED 诊断看“当前设备收到的字节数”(见根目录 README 的“PS/2 键鼠”一节)。
USB 鼠标在这个核上不现实(协议栈放不下)。
踩过的坑(值得写进报告):cpu21_ps2_mouse.v 早期把 11 位帧整体右移、又在
停止位还没移进来时就判 frame_q[10],并且把 PS/2 的奇校验当成偶校验判,结果
真设备发来的帧全部被丢掉 —— 上板表现就是“插上鼠标毫无反应、收到的字节数恒为 0”,
很容易误判成硬件/跳线问题。现在帧按位的下标存(frame_q[i] = 第 i 位)、停止位
单独判(它就在当前拍的 ps2_data 上)、校验按奇校验判,并且
srcs/tb/tb_ps2_mouse.v / tb_ps2_kbd.v 两个自测里都带“故意发错校验 → 必须丢帧”
的用例。
6. 验证状态(说实话)
| 部分 | 状态 |
|---|---|
| C 工具链链路 | 已实测通过:text=4608 B, data=0, bss=2508 B,1152 条指令全部通过 ISA 检查 |
游戏逻辑(布雷、count_adj、三档难度、draw_tile) |
PC 侧独立测试通过(3 难度 × 40 局重算比对 + 端到端显示测试) |
仿真显示(tb_saolei / tb_saolei_play + tools/fb2png.py) |
通过:整屏 dump 能还原成 PNG,hard 盘面(30×16)整屏都能画出来 |
cpu21_mmio.v / cpu21_vga.v / cpu21_seg7.v / cpu21_pointer.v / 板级顶层 / XDC |
xvlog 编译通过;已综合、已上板 |
| 上板实测 | VGA 画面、开始界面、三档难度、开格/插旗、板载按键均已实测可用 |
| PS/2 键鼠 | 接收器逻辑有自测(tb_ps2_mouse / tb_ps2_kbd);板载 USB HOST 口待复测 |
建议验证顺序:① 跑 pipelined-cpu/tools/run_regress.ps1 确认跑马灯/中断/指令测试仍全
PASS;② 定义 SAOLEI_ROM 跑行为仿真(tb_saolei 会自动打完一局并 dump 显存);
③ 上板看 VGA;④ 接键鼠时先用 srcs/tb/tb_ps2_* 确认解码无误,再看 LED 诊断确认板载
桥接芯片到底有没有转发你插的那个设备。
7. 已知可改进项
- 难度:已按微软扫雷做成三档 ——
EASY9×9/10 雷、NORMAL16×16/40 雷、HARD30×16/99 雷。盘面尺寸是运行期变量(g_bw/g_bh/g_mines/g_ncells), 开始界面上点那一行就即时切换、重新画盘面;所有索引走IDX_R/IDX_C宏。 再想更大就要动bfs_q的打包方式(数据 RAM 只有 4 KB)。 - 输入:板载 5 向按键、PS/2 鼠标、PS/2 键盘三种事件源在顶层合并成同一种格式 (±8 = 一格),键鼠用 SW15 切换(见上文“操作”)。
- 文字显示:显存每格 2 字节(字形[5:0] + 颜色[2:0]),RTL 内置 A-Z 和数字/游戏
符号字模,开始界面显示
MINES、EASY、NORMAL、HARD、START。注意字符颜色 不能取黑色(黑字画在黑背景上等于看不见)。 - 回归覆盖:
run_regress.ps1里还没有扫雷的自动测试项(PS/2 接收器有单独的tb_ps2_mouse/tb_ps2_kbd);也还没有“自动玩一局 + 显存 dump + PNG 比对”的 一键回归。