Files
risc-v-cpu/saolei/README.md
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

207 lines
15 KiB
Markdown
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.
# 扫雷(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()`(**背景程序**,从头到尾一直运行),它画开始界面
并等鼠标。整套交互是这样串起来的:
```mermaid
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)。
```powershell
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`。
编译参数里有三处是这颗核/工具链的坑,已经写在脚本注释里:
1. `-march=rv32im_zicsr` —— GCC 15 起 `rv32im` 不含 Zicsr,`csrrsi` 会报
`unrecognized opcode ... extension 'zicsr' required`;
2. **不要禁链接器松弛**(别加 `-mno-relax`/`-Wl,--no-relax`)—— gcc 的函数调用默认是
`auipc ra,%pcrel_hi` + `jalr`,要靠链接器松弛压成一条 `jal`;禁掉后 35 条 `auipc`
会留在代码里(这颗核没有)。用 `-msmall-data-limit=0` 保证松弛不会引入 gp 相对寻址;
3. 中断入口地址必须写在**段声明**上(`.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. 怎么跑起来
**仿真**(用你现成的测试平台):
```powershell
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. 已知可改进项
- **难度**:已按微软扫雷做成三档 —— `EASY` 9×9/10 雷、`NORMAL` 16×16/40 雷、
`HARD` 30×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 比对”的
一键回归。