Files
risc-v-cpu/saolei
Alpha f50639e90f feat(saolei): 新增纯 C 扫雷(MMIO 显示 + 中断读鼠标),扩展 CPU 外设并清理仓库
## 背景

在原有五级流水 CPU 的基础上,用一颗 RISC-V 工具链把"纯 C 写的扫雷"编译成机器码
固化进片内 ROM 运行,并为此把 MMIO 外设从"只有一个 LED 寄存器"扩展成
显示(VGA 显存)+ 七段数码管 + 鼠标事件(含中断)的完整子系统。
原 LED MMIO 的语义保持不变,原有跑马灯/中断/指令回归不受影响。

## 硬件(pipelined-cpu/srcs)

- cpu21_mmio.v:扩展成 MMIO 中枢,新增
  * 七段数码管寄存器(0x1004,8 位十六进制)
  * 鼠标寄存器组(0x1100+):STATUS/DATA/ACK/CTRL/INPUT/TIME/FRAME/EVCNT/LEVEL/MAGIC
    —— 含 16 项事件 FIFO 与 mouse_irq_o(读空自动回 0,不丢事件)
  * 显示控制器(0x1140+):CTRL(光标可见/背景色)、CURSOR(列、行)
  * 显存窗口(0x2000-0x27FF):写端口输出到板级顶层的双口 RAM,读保持组合读
  * LEVEL(0x1120):软件中断寄存器,写 1 拉起 level_irq_o(用作"关卡入口")
  * 地址译码、字节写合并、读数据 mux 全部按原风格实现
- 新增外设 RTL:cpu21_vga.v(640x480@60 按格子扫描,8x8 字模放大 2 倍)、
  cpu21_seg7.v(8 位数码管动态扫描)、cpu21_pointer.v(5 向按键→鼠标事件,1 ms 去抖
  + 事件排队)、cpu21_ps2_mouse.v(可选真鼠标,PS/2 11 位帧接收 + 3 字节包解析)
- cpu21_riscv_redirect_int_bpb.v:新增外设总线端口(显存写/读回、鼠标事件输入、
  中断输出、数码管数据、帧脉冲),例化扩展后的 cpu21_mmio
- cpu21_board_top.v:接入显存双口 RAM + VGA + 数码管 + 鼠标事件源,中断接线为
  irq_i = {SW2, LEVEL|SW1, 鼠标|SW0};ROM 宏切换(定义 SAOLEI_ROM 加载扫雷程序);
  ROM_WORDS 1024→8192
- cpu21_board_top.xdc:新增按键、SW[3]、8 位数码管、VGA、PS/2 的全部引脚约束
- tb_board_top.v:新端口显式接 0/1,避免悬空影响原跑马灯回归
- create_project.tcl:把 cpu21_saolei_rom.vh 按 Verilog Header 条件加入

## 软件(saolei/)

- sw/mmio.h:全部 MMIO 地址与格子编码(换地址只改这一个文件)
- sw/saolei.c:纯 C 扫雷 —— 背景"伪操作系统" main()(开始界面)+ 关卡 level_main()
  (在 IRQ1 里跑完整局)+ 棋盘位图/布雷(首点安全)/BFS 展开/插旗/胜负判定/逐格绘制
- sw/isr.c:**中断读鼠标的代码**。三个固定地址入口:
  * isr1_entry(.text.isr1=0x100)关卡入口:保存全部调用者保存寄存器 +
    csrrsi 打开 MIE(让鼠标中断可嵌套)→ level_main
  * isr2_entry(.text.isr2=0x200)鼠标读取:只存 ra → 纯 C 主体 isr_body
  * isr3_entry(.text.isr3=0x300)备用:直接 uret
- sw/start.c:设 sp、清 .bss、进 main(只有这一处必要内联汇编)
- sw/linker.ld:复位 0x0、三个中断入口 0x100/0x200/0x300、栈顶 0x0FFC;
  用 ASSERT 禁止 .data/.rodata(这颗核读不到 ROM 里的数据)

## 构建与工具链适配(saolei/tools)

- build.ps1:一键 C → .o → ELF → .hex + cpu21_saolei_rom.vh,任一步失败立刻停止
- check_isa.py:用"指令白名单"逐条检查反汇编,出现本核未实现的指令即报错并给修复提示
- elf2hex.py:裸二进制 → $readmemh hex + ROM 初始化头文件
- 三处针对本核的关键配置(都已写进脚本注释):
  1) -march=rv32im_zicsr(GCC 15 起 rv32im 不含 Zicsr,csrrsi 会报错)
  2) 不能禁链接器松弛:gcc 函数调用默认是 auipc+jalr,靠松弛压成 jal,
     禁掉后 35 条 auipc 会留在代码里(本核没有 auipc);
     用 -msmall-data-limit=0 防止松弛引入 gp 相对寻址
  3) 中断入口地址必须写在段声明上(.text.isr1 0x100 : ...),
     只写 ". = 0x100;" 会被 ld 忽略(实测 isr1 掉到了 0x14)
- 实测结果:text=3368B、data=0、bss=1436B,842 条指令全部通过 ISA 检查,
  入口地址 _start=0x0 / isr1_entry=0x100 / isr2_entry=0x200 / isr3_entry=0x300

## 仓库清理

- 删除被工程取代的 saolei/hw、saolei/constrs(外设已并入 srcs/rtl)
- 删除全部 Vivado 生成物:.xpr、.runs、.sim、.cache、.hw、.ip_user_files、.Xil、
  vivado.jou/log —— 需要工程时用 source create_project.tcl 一键重建(约 0.3 MB 源文件)
- .gitignore 增加 saolei/build/(构建产物可一键重建)

## 文档

- README:更新目录结构、仓库约定(不放生成物)、MMIO 地址表、文件与模块划分表、
  上板说明,新增"扫雷程序(saolei/)"章节
- saolei/README.md:重写 —— 程序结构(背景/关卡/鼠标 + 三个中断入口)、
  本核指令子集与对应写法、构建步骤与实测数据、MMIO 地址表、上板/仿真步骤、
  操作说明、验证状态与已知可改进项

## 验证状态(如实说明)

- 已实测:C 工具链链路(编译/链接/ISA 检查/hex 生成/入口地址逐条核对)
- 未验证:所有 RTL 未经综合与仿真(本机无 Vivado),C 程序也未在仿真器/硬件上运行过;
  建议先跑 tools/run_regress.ps1 确认原回归仍全 PASS,再用 SAOLEI_ROM 宏做行为仿真
2026-09-16 20:11:00 +08:00
..

扫雷(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()(背景程序,从头到尾一直运行),它画开始界面 并等鼠标。整套交互是这样串起来的:

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。

编译参数里有三处是这颗核/工具链的坑,已经写在脚本注释里:

  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-0x27FF 显存:40 列 × 30 行、每格 1 字节(只用到前 1200 字节)

LEVEL 是软件中断寄存器:写 1 拉起 irq_i[1](关卡入口),关卡返回前写 0 清掉, 否则下次点"开始"没有新的上升沿。MAGIC(0xCAFE0001/2)可以读来自检 MMIO 是否活着。

格子编码 1 字节 = [3:0] 字形 [6:4] 颜色 [7] 保留;字形 0 空白、1..8 数字、9 未揭开、 10 旗、11 雷、12 爆炸、13 问号、14 横线、15 实心;颜色是 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 向按键):

操作 按键
移动光标 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 工具链链路 已实测:编译/链接/ISA 检查/hex 生成/入口地址逐条核对 全部通过
saolei.c / isr.c / start.c / linker.ld 逻辑对着 RTL 写;未在仿真器/硬件上跑过
cpu21_mmio.v 扩展、cpu21_vga.v、cpu21_seg7.v、cpu21_pointer.v、板级顶层、XDC 未综合、未上板

建议验证顺序:① 跑 pipelined-cpu/tools/run_regress.ps1 确认原来的跑马灯/中断/指令 测试仍全 PASS;② 定义 SAOLEI_ROM 跑行为仿真,看有没有往 0x2000 起的显存写字节、 按键后有没有跳进 0x200;③ 再上板。

7. 已知可改进项

  • 屏幕上没有字母:字模是 4 位字形(0-15),所以"开始"是绿色按钮条 + 光标框而不是 "START" 字样。要显示文字就把格子编码扩成 5 位字形(mmio.h 的 CELL() 与 cpu21_vga.v 的字模表各改一处,颜色降到 3 位)。
  • 棋盘固定 16×16、40 雷(16 是 2 的幂,索引全用移位,一次除法都不用);要改大小就改 saolei.c 顶部的 BW/BH/NMINES。
  • run_regress.ps1 里还没有扫雷的自动测试项。