显示(RTL) - cpu21_vga.v:显存从"每格 1 字节"改成"每格 2 字节"(第 1 字节是 6 位字形、 第 2 字节是 3 位颜色),字地址由 cidx[10:2] 改为 cidx[10:1](1 字 = 2 格); 字模表由 4 位扩到 6 位(0..63),新增 A-Z 大写字母(16..41),原 0..15 的 数字/雷/旗/问号/横线/实心字形不变 - cpu21_mmio.v:显存窗口 0x2000-0x27FF -> 0x2000-0x2FFF(300 -> 600 字), fb_widx 相应取 addr_i[11:2] - cpu21_board_top.v:FB_WORDS 300 -> 600(1200 -> 2400 字节) 软件 - mmio.h:fb_write() -> fb_write_cell(idx, glyph, color)(一格写两个 sb); 删掉 CELL(),新增 G_A..G_Z 字形常量 - saolei.c:draw_cell / clear_screen 改用 fb_write_cell;开始界面显示 MINES / EASY / NORMAL / HARD / START 文字(START 直接写在绿色按钮条上), 不再是"两排雷图案 + 纯色条" - 难度选择:新增 g_difficulty / g_mines,三档 20 / 40 / 60 雷;place_mines、 胜利用的 opened_cnt 比较、toggle_flag 的剩余雷数都改读 g_mines;点难度那一行 即切换并重画菜单,点 START 才把雷数交给关卡 - 全部索引运算仍只用移位/掩码(棋盘固定 16x16),没有引入除法 配色(修"数字 1 看不见") - 数字 1 用 C_BLUE、游戏背景原来也是 C_BLUE,调色板里是同一个深蓝,所以数字 1 完全看不见;现在菜单与关卡背景统一改成 C_BLACK,胜负闪动仍是绿/红/黑, 数字本身的配色不动 工具与文档 - build.ps1:Python 启动器兼容 —— 有 py 用 py,没有就回退到 python (本机只有 python,之前 check_isa.py / elf2hex.py 两处都会中断) - saolei/README.md:显存表(0x2000-0x2FFF、每格 2 字节)、格子编码(6 位字形、 16..41 是 A-Z)、难度说明同步更新 - README.md / cpu21_board_top.v:仅格式化工具重排(Markdown 表格对齐、 if/else 合并成一行),无语义变化 验证:build.ps1 全流程通过(972 条指令 ISA 检查全过;text=3888 B / data=0 / bss=1444 B;入口 _start=0x0、isr1/2/3=0x100/0x200/0x300);xvlog 不带宏与带 SAOLEI_ROM 两种配置编译均无错;tools/check_rtl_text.ps1 体检 OK。 未验证:未跑行为仿真、未上板。
184 lines
12 KiB
Markdown
184 lines
12 KiB
Markdown
# 扫雷(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`(可选真鼠标)、`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 向按键):
|
||
|
||
| 操作 | 按键 |
|
||
| ------------------------------------ | --------------------------------------- |
|
||
| 移动光标 | `BTNU` / `BTND` / `BTNL` / `BTNR` |
|
||
| 左键(揭开;在开始界面上 = 开始) | `BTNC` |
|
||
| 插旗模式(拨上去后左键 = 插/取消旗) | `SW0` 或 `SW3` |
|
||
| 返回上一层(关卡内) | 把光标移到盘面**左上角黄格**再按 `BTNC` |
|
||
|
||
**鼠标**:默认按键;想用真鼠标就把顶层参数 `USE_PS2_MOUSE` 设为 1(需要 Digilent
|
||
Pmod PS2 插在 JA + 一个 PS/2 鼠标)。`cpu21_ps2_mouse.v` 只实现了**接收**(发 0xF4
|
||
初始化要靠鼠标产生的时钟脉冲,不是固定延时能糊的,所以没写假的);多数鼠标上电即上报,
|
||
需要 0xF4 的那种得自己补。USB 鼠标在这个核上不现实(协议栈放不下)。C 程序两种方式
|
||
完全不用改。
|
||
|
||
## 6. 验证状态(说实话)
|
||
|
||
| 部分 | 状态 |
|
||
| ------------------------------------------------------------------------------------ | ------------------------------------------------------ |
|
||
| C 工具链链路 | 本次修改后未验证:当前环境缺少 RISC-V 交叉编译器 |
|
||
| `saolei.c` / `isr.c` / `start.c` / `linker.ld` | 本机 GCC 语法检查通过;目标 ISA 构建待工具链恢复后执行 |
|
||
| `cpu21_mmio.v` 扩展、`cpu21_vga.v`、`cpu21_seg7.v`、`cpu21_pointer.v`、板级顶层、XDC | `xvlog` 编译通过;未综合、未上板 |
|
||
|
||
建议验证顺序:① 跑 `pipelined-cpu/tools/run_regress.ps1` 确认原来的跑马灯/中断/指令
|
||
测试仍全 PASS;② 定义 `SAOLEI_ROM` 跑行为仿真,看有没有往 `0x2000` 起的显存写字节、
|
||
按键后有没有跳进 `0x200`;③ 再上板。
|
||
|
||
## 7. 已知可改进项
|
||
|
||
- **文字显示**:显存现在每格占 2 字节,字形编号扩展到 6 位,RTL 内置 A-Z 和数字/游戏
|
||
符号字模,开始界面显示 `MINES`、`EASY`、`NORMAL`、`HARD`、`START`。
|
||
- 棋盘固定 16×16,开始界面可选 20/40/60 雷(16 是 2 的幂,索引全用移位,一次除法都不用)。
|
||
- `run_regress.ps1` 里还没有扫雷的自动测试项。
|