Author SHA1 Message Date
Alpha 1e5cb54c00 docs(readme): 记录上板实测出来的 PS/2 方向约定(鼠标 dy 取负)
上板已经确认:不定负号的表现就是“左右正常、上下反向”,不是解码错;
另外把“键鼠静止时一个字节都不发”写成显式提醒,避免再被误判成硬件坏。
2026-09-23 22:50:29 +08:00
Alpha 28c696ece2 fix(ps2): 鼠标 dy 方向取负(PS/2 正 dy = 上,软件正 dy = 下);键盘补 W/A/S/D 别名
上板实测:鼠标已能操作(说明“奇校验 + 帧按位的下标对齐”那个修复就是根因),
但上下是反的、左右正确 —— 这是两套约定的差异,不是解码错:
  * PS/2 鼠标原生约定:dx 正 = 右、**dy 正 = 上**
  * saolei.c 的约定:正 dx = 列 +1、**正 dy = 行 +1(向下)**,和 cpu21_pointer 的
    BTND = +8 一致
所以 cpu21_ps2_mouse 在拼包时把 dy 取负(dx 不动),三种事件源(板载按键 / PS/2 鼠标 /
PS/2 键盘)方向统一。

键盘:另加 W/A/S/D = 方向键(与 tools/play_saolei.tcl 里的别名保持一致),这样用普通
字母键也能测。

自测同步更新(都 ALL PASS):
  * tb_ps2_mouse:改发“鼠标向上”的 dy=+8,事件里必须得到 dy=-8;向下 dy=-5 -> +5
  * tb_ps2_kbd:新增 W(1D) -> dy=-8 用例
2026-09-23 22:50:19 +08:00
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
Alpha 9bbe6bc7eb debug(board): PS/2 诊断升级 —— 用 LED 直接看两根线的电平与活动
上板现象:板载按键正常(事件链没问题),USB HOST 口插罗技鼠标后 rx_count 一直是 0。
为了区分"芯片根本没发数据"和"我们的解码有问题",把 LED 高位改成:

  LED[15:6] = PS/2 收到的字节数低 10 位
  LED[5]    = ps2_clk 有边沿活动(时钟在动就快速翻转)
  LED[4]    = ps2_data 当前电平
  LED[3]    = ps2_clk  当前电平
  LED[2:0]  仍是三路中断指示

(顺带确认:F4/B2 在官方母版里只属于 USB HID (PS/2),没有被 SD/以太网等复用。)
2026-09-23 22:03:50 +08:00
Alpha 77dd102675 feat(board): 板载 USB HID 口接 PS/2 鼠标,与板载按键并存
Nexys4DDR 的 USB HOST 口由板上芯片把 USB 键鼠转成 PS/2 信号(官方母版
Nexys4DDR_Master.xdc 标明 ps2_clk = F4、ps2_data = B2),所以不用写 USB 协议栈。

- cpu21_board_top.v:
  * USE_PS2_MOUSE 默认打开;原来是 if/else 二选一(选鼠标就没按键),现在
    cpu21_pointer 与 cpu21_ps2_mouse 同时例化,两路事件在顶层合并成 ev_valid/
    ev_dx/ev_dy/ev_btn(同拍优先 PS/2),板载按键一个不删
  * 新增 DEBUG_PS2_RX_LED(默认 1):把 PS/2 收到的字节数显示到 LED[15:3]
    (LED[2:0] 仍是中断指示)。没有调试器时靠它判断 PS/2 到底有没有数据:
    鼠标一动数字就跳 = 通了;一直是 0 = 根本没收到
- cpu21_board_top.xdc:ps2_clk/ps2_data 从 Pmod JA(C17/D18)改到板载 USB HID
  (F4/B2),并在注释里留了改回 Pmod 的方法

软件侧不用改:鼠标事件与按键事件格式相同,且 PS/2 的 dx/dy 天然满足
"8 个计数 = 一格"(saolei.c 里就是 acc_x += dx; while (acc_x >= 8) 移动一格)。
额外好处:鼠标右键 = 插旗(handle_event 本来就支持)。
2026-09-23 21:50:18 +08:00
Alpha 64428bdd85 feat(tools): 仿真调试台适配新版显存,并修好 xsim 里跑 python 的环境问题
tb_saolei.v / tb_saolei_play.v:
- dump 从 1200 字节(1 字节/格)改成 2400 字节(每格 2 字节)
- 变量地址按新 ELF 更新(g_state 0x7AC、mine_bm 0x86C、g_ncells 0x8B4 ...)
- 已揭开格数直接读 opened_cnt(0x7B4),通关目标改成 g_ncells - g_mines

fb2png.py:
- 支持每格 2 字节(byte0 字形[5:0] / byte1 颜色[2:0])
- 字库不再手工抄表,改成直接解析 cpu21_vga.v 的 font_row(),RTL 加字模会自动跟随
- 字形 16..41 显示成 A-Z

play_saolei.tcl:
- 用绝对路径找系统 Python:xsim 的环境里 py 会选中 Vivado 自带的 python3.8,
  报 SRE module mismatch
- 清掉 PYTHONHOME/PYTHONPATH,否则系统 python 也会去 Vivado 的 lib 找模块
- render/save_shot 补 global python

实测:hard 模式 30x16 棋盘在终端正常显示,点击展开的区域横跨屏幕第 13~22 行
(含中线以下的 8 行)-> 证实 FB_WORDS 那个"只能写前 15 行"的 bug 已修好
2026-09-23 21:21:32 +08:00
Alpha 10d4836ad7 fix(saolei): 修 hard 模式光标到某一格就跳回最左边(列只有 5 位)
现象:只看 hard 模式时,光标移到某一排的某个格子后再往右,会跳回最左边。

原因:光标的列字段只有 5 位。
- 软件 update_cursor_reg():REG_CURSOR = (cur_c & 31u) | ...
- RTL cpu21_vga.v:cursor_here = (cursor_i[4:0] == ccol) && ...
hard 棋盘宽 30、左上角列 5,光标列能到 35,35 被掩成 3,于是画在第 3 列。
初级/中级列最大 25 以内,所以只有 hard 暴露出来;背景界面光标能走到列 39,
同样受影响。

改法:
- saolei.c:列掩码 31u -> 63u(6 位,覆盖 0..39)
- cpu21_vga.v:cursor_i[4:0] -> cursor_i[5:0]
2026-09-23 21:13:35 +08:00
Alpha 7e13571744 fix(saolei): 修显示/难度/数字三处问题(只本地提交)
1) 显示:VGA 的像素色 = 字模点 ? 字符色 : 背景色,而背景是黑的,所以 START 按钮上
   那几个 C_BLACK 字母的字形点和空白处全黑,整格看不见。改成黄色。
2) 难度:三档改成微软扫雷的真实尺寸 9x9/10、16x16/40、30x16/99(原来三档都是
   16x16,只有雷数不同)。棋盘尺寸由编译期常量改成运行期变量
   (g_bw/g_bh/g_board_r0/g_board_c0),所有索引改用 mul/divu/remu(这颗核都有),
   棋盘自动居中;BFS 加 seen_bm 去重,避免重复入队把队列撑满丢格。
3) RTL:cpu21_riscv_redirect_int_bpb 例化 cpu21_mmio 时漏传 FB_WORDS,mmio 用默认
   300,fb_ok 把显存写卡在前 300 字(前 15 行),屏幕下半部分永远刷不出来。
   补上 .FB_WORDS(FB_WORDS) 并给顶层加参数,mmio 默认值也改成 600。

验证:
- build.ps1:编译 + check_isa 指令白名单全部通过(新增的 mul/divu/remu 都合法)
- 棋盘逻辑(PC 上 gcc 跑 _tmp/logic,不改仓库):三档 x 40 局 x 全部格子,
  count_adj 与独立重算逐格一致,雷数、首点 3x3 安全区都正确
  -> 数字与雷位没有逻辑矛盾,看到的 3 旁边 4 个雷 应是显示层问题
2026-09-23 20:54:28 +08:00
Anccend cfcf2f4923 feat(saolei): 显存扩成每格 2 字节 + 内置 A-Z 字模;开始界面加文字、难度选择和黑背景
显示(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。
未验证:未跑行为仿真、未上板。
2026-09-23 19:23:45 +08:00
Alpha db0a174bb7 docs: 仿真演示步骤补上 _tmp 目录的创建命令,复制粘贴即可跑 2026-09-19 20:34:10 +08:00
Alpha 694446d8f0 docs: README 补上扫雷烧板步骤与无显示器仿真演示命令
- 新增「扫雷:怎么烧到板上」:build.ps1 + create_project_saolei.tcl + 板上按键表
- 新增「扫雷:没有 VGA 显示器怎么玩」:xvlog/xelab/xsim 三条命令、play_saolei.tcl 的
  k c / k u / k f 操作、每步自动出 PNG(shots/NNN_<按键>.png 与 shots/latest.png)
- 顺带清理工作区临时产物(_tmp 下的 dump/png/log、.Xil、vivado.jou/log)
2026-09-19 20:33:14 +08:00
Alpha 553171e776 fix(vga): 光标框改成黄色常亮 2 像素,一眼可见
原来是 1 像素浅灰 + blink 闪动(约 1.3 s 周期):在白色未揭开格上几乎看不清,
且仿真截图有一半概率正好截到灭的相位,导致看图时找不到指针。
- cpu21_vga.v: on_border 1px -> 2px,去掉 blink(常亮),颜色 7(浅灰) -> 6(黄)
- fb2png.py: 渲染器同步同样的规则
实测:仿真截图里黄色光标框清晰可辨
2026-09-19 20:28:39 +08:00
Alpha 713ee99941 feat(saolei): 交互玩扫雷每一步自动出 PNG
- play_saolei.tcl: k 每执行一个按键动作就存 shots/NNN_<按键>.png,并覆盖
  shots/latest.png(在 VS Code 里开着 latest.png 即可每步自动刷新);新增 view 命令
- fb2png.py: 新增 --quiet(只写 PNG 不打印,供脚本批量截图)
- tb_saolei*.v: 仅格式调整(case 分支拆行),无语义变化
实测:k cuuuuc -> shots/001_c.png ... 006_c.png,每张 640x480 正常
2026-09-19 20:01:28 +08:00
Alpha 132c9202f6 feat(saolei): 命令行交互式玩扫雷(无需 VGA 显示器)
- srcs/tb/tb_saolei_play.v:受 cmd.txt 驱动的交互式 TB,每 100us 轮询一个按键字符,
  每 1ms 把显存 dump 到 now.txt
- tools/play_saolei.tcl:xsim 里 k c / k u / k f 一步一按键,自动把 now.txt 画到终端
- saolei/tools/fb2png.py:新增 --term,用 ANSI 24 位色在终端画 40x30 棋盘,光标格反色
- .gitignore:放行 create_project_saolei.tcl
实测:k c 进关卡、k uuuu 光标精确移动 4 格、k c 首击展开出数字 1/2
2026-09-19 19:54:56 +08:00
Alpha 0908580e46 tcl: 新增扫雷专用建工程脚本 + 修文本自检误报
- create_project_saolei.tcl:独立工程 pipelined-cpu-saolei,只装配扫雷所需源文件,
  sources_1/sim_1 都固定定义 SAOLEI_ROM,sim_1 顶层 = tb_saolei;先清理旧工程再新建
  (实测:建工程 + 综合均通过,0 error / 0 critical warning)
- check_rtl_text.ps1:不再把 Verilog 延时语法 #10 / #(expr) / #delay 误报成粘贴正文
- create_project.tcl:默认程序切回中断测试 ccab
2026-09-19 19:50:37 +08:00
Alpha b8c62c2726 tb: 无显示器扫雷联调套件(完整一局通关 216/216)
- srcs/tb/tb_saolei.v:VGA 帧缓冲截图、RAM 变量观察、PC 热点、跳转探针,
  完整一局流程(点 START -> 首点击放雷 -> 带落位校验的全盘扫描 -> 通关)
- saolei/tools/fb2png.py:把仿真 dump 渲染成 640x480 PNG(无第三方依赖)
2026-09-19 19:43:07 +08:00
Alpha 47d2f22d07 fix(saolei): 修好鼠标事件链路三处 RTL bug —— 仿真里扫雷已能正常游玩
1) cpu21_pointer.v: sel 的位映射写错了(pend[2]->bit1, pend[1]->bit0, pend[0]->bit2),
   而输出读的是 sel[1]=-dx / sel[2]=+dy / sel[3]=-dy / sel[0]=+dx。
   后果:方向全乱(按下变左移、按右变下移),更致命的是清除逻辑
   press_pend_q <= pend_next & ~sel 清不掉被选中的那一位 -> ev_valid_o 永久为 1
   -> 鼠标中断线一直挂着 -> CPU 只收到一个上升沿、之后再无事件。改成一一对应。
2) cpu21_board_top.v: 鼠标中断与 LEVEL 软件中断的接线正好对调
   (mouse 接 irq_vec[0]=IRQ1=0x100=关卡入口, level 接 irq_vec[1]=IRQ2=0x200=鼠标)。
   后果:第一个鼠标事件把关卡叫起来画棋盘, 而鼠标 ISR 永不执行 -> FIFO 不弹空。
   按 linker.ld/mmio.h 约定交换;拨码开关 SW[2:0] 仍按位 OR 进来。
3) cpu21_mmio.v: 同步中断线注释。

仿真验证(tb_saolei + fb2png): 事件队列正常出入(h/t 同步增长)、光标方向正确、
点击 START 进关卡、关卡内点击揭开格子(fb_we +90)、数字场 1/2/3 与大片 BFS 展开均正确
2026-09-19 19:33:18 +08:00
Alpha 3f765ae4c5 fix(saolei): 修显存窗口译码 + 段间空洞填 NOP; 新增无显示器调试台(仿真截图)
- cpu21_mmio.v: sel_fb 原写成 addr[12] && !addr[11](那是 0x1000-0x17FF),
  导致 0x2000+ 的显存读写全不命中, 而鼠标/显示寄存器写反而落进显存。
  改为 bit13=1 && bit12=0 && bit11=0 后扫雷画面才正常:
  修复前仿真 fb_we 只有 6 次写(画面全空), 修复后开始界面 1246 次、进关卡后 2775 次
- cpu21_board_top.v: 新增 POINTER_SAMPLE_DIV 参数(默认仍为上板的 50000),
  仿真传 50 即可让按键在几十微秒内被识别(defparam 对 generate 块内实例无效)
- saolei/tools/elf2hex.py: .text.isr1/2/3 之间的空洞(0 字)填 NOP —— objcopy 留下的
  0x00000000 在本 CPU 上等同自跳指令, PC 掉进去就出不来
- saolei/tools/fb2png.py + srcs/tb/tb_saolei.v: 无 VGA 显示器的调试台 —— 跑板级顶层、
  按键当鼠标、把 40x30 显存 dump 成文本, 再按 cpu21_vga.v 的字库/调色板还原 PNG,
  并附 ASCII 图 + RAM 变量视图 + PC 热点直方图
2026-09-19 19:26:26 +08:00
Alpha 4ef3960553 feat(tcl): create_project.tcl 自动清理旧工程后重建
- 新增 clean_first 开关(默认 1):先删掉旧工程与所有生成目录再新建,
  只删生成物(*.xpr/*.srcs/*.runs/*.sim/*.cache/*.hw/*.gen/*.ip_user_files/
  .Xil/xsim.dir/*.lpr/vivado*.jou|log|str/webtalk.*),srcs/、tools/、programs/ 不动
- 被别的 Vivado 占用的工程目录删不掉时,给出明确提示并中止(不再抛 Vivado 原始异常)
- create_project 加 -force,残留目录也能覆盖
- 实测:空目录建工程成功;连续第二次运行自动清理 7 项后重建成功,综合通过
2026-09-19 16:26:15 +08:00
Alpha e44680a114 feat(board): 新增课程新版中断测试上板(多级中断 + 附加 SLL/SB/BLTU)
- programs/risc-v-intrupt-sllsb.asm/.hex: 课程原始程序(344 字,hex 已去掉 Logisim
  的 v2.0 raw 头),保留原文件以便追溯
- tools/adapt_intrupt_sllsb.py: 把它适配到本 CPU 上板(只改 ROM 内容,地址不变)
  - 课程用 addi a7,zero,34 + ecall 显示,本 CPU 里那是空操作 -> 每条显示点原地换成
    一次调用:主程序 jal ra,DISP_DLY;ISR 用 add a7,a0,zero + jal a0,DISP_DLY_ISR
    (三个 ISR 都没保存 ra,所以 ISR 版把链接寄存器放 a0、显示值放 a7)
  - DISP_DLY / DISP_DLY_ISR 各 8 条,追加在 uret 之后,中断入口 0x0AC/0x240/0x3E8 不变
  - 返回地址存 sp+256(越过 ISR 最多 192 字节的栈区),嵌套中断也破坏不了
  - 脚本自带跳转目标自检(曾发现 ISR 侧 jal 偏移少算一条指令的问题)
- srcs/board/cpu21_board_top.v: 新增 RISC_V_INTRUPT_SLLSB_ROM 分支(ROM 选择、
  CPU 分频用 CLK_DIV_INTRUPT、中断向量 0x0AC/0x240/0x3E8)
- srcs/board/risc-v-intrupt-sllsb.vh: 生成的 ROM 初始化(1024 字,程序 360 字)
- create_project.tcl: 新增 use_intrupt_sllsb 开关(默认 1,已把 ccab 置 0)

实测:Vivado 2020.2 建工程 + 综合通过(sources_1 verilog_define =
RISC_V_INTRUPT_SLLSB_ROM,synth_design Complete! 100%)
2026-09-19 16:10:19 +08:00
Anccend f7844b7b1c feat(board): 中断综合测试程序上板 + 可变分频/刷新率;README 大幅扩写
板级顶层(srcs/board/cpu21_board_top.v)
- ROM 增加 RISC_V_INTRUPT_CCAB_ROM 分支,中断入口 0x0AC/0x1E4/0x308;
  与 SAOLEI_ROM / BOARD_ROM_FAST / 默认跑马灯构成 ifdef 链
- 新增参数 CLK_DIV_INTRUPT:跑中断测试程序时用更慢的 CPU 时钟(默认 32
  = 3.125 MHz),让数码管上每个数值停留约 160 ms 便于观察
- 数码管扫描分频随 CPU 分频缩放(SEG_REFRESH_DIV = 100000/CPU_DIV),
  保证每位数码管仍是 125 Hz(不闪);数码管改显示 MMIO 的 led_data;
  LED[2:0] 改为锁存式中断指示(请求上升沿点亮、对应 ISR 执行 uret 后熄灭)
- 复位同步器/led_we 指示等注释同步更新;srcs/rtl/cpu21_seg7.v 分频计数
  加宽到 20 位以容纳随主频变化的 REFRESH_DIV

工具与程序
- tools/adapt_intrupt_ccab.py:把课程中断测试镜像适配成上板 ROM。六处适配
  (ISR 停机 ecall、显示改 MMIO+DISP_DLY、ISR2/ISR3 出栈顺序、ISR3 SB 测试
  用未保存寄存器、ISR 破坏 ra 的 DISP_DLY_ISR),全部 2 字换 2 字,
  不移动任何指令地址;带断言与 jal 目标自检
- programs/risc-v-intrupt-ccab.asm/.hex:课程中断综合测试程序(含六处适配说明)
- create_project.tcl:新增 use_intrupt_ccab / use_saolei 开关,自动把对应
  ROM 头文件按 Verilog Header 加入 sources_1 并定义宏
- tools/rars_f0c874c.jar:课程 RARS 汇编器移到 tools/

文档(README.md)
- 新增"各模块的实现方式":按流水线主体 / 外设 / 板级逐模块讲实现要点
- 新增"内置程序与宏的用法":ROM 宏表(含各自的中断入口)、GUI/Tcl/命令行
  三种定义方式、中断程序的适配流程与仿真验证方法
- 新增"分频与刷新率一览":CLK_DIV / CLK_DIV_INTRUPT / SEG_REFRESH_DIV /
  led_we 时长 / DISP_DLY 延时常量分别在哪里改、怎么换算
- 修正引脚表(LED[2:0] 中断指示、led_we 0.34 s、CPU_DIV)与 ROM/RAM 说明
  (ROM 8192 字 = 32 KB),补充中断程序上板现象与 ROM 头文件清单

验证:不带宏 / 带 RISC_V_INTRUPT_CCAB_ROM 两种配置 xvlog 编译均无错误;
三路嵌套与 IRQ3 抢占 ISR1 的仿真通过(level 逐级升降、uepc 正确恢复、
halted=0);tools/check_rtl_text.ps1 体检 OK。
2026-09-19 15:44:47 +08:00
Alpha 4f82331dbe docs(tcl): 说明只用 -part 建工程、不依赖 Nexys4DDR 板卡文件(附可选 -board_part 写法) 2026-09-18 16:32:30 +08:00
Alpha 189d028943 chore: 忽略根目录下批量运行 Vivado 产生的 vivado*.jou/log 2026-09-18 16:17:52 +08:00
Alpha 3512238965 fix(rtl): 修 cpu21_ps2_mouse 三处真 bug, 综合已通过 (0 errors / 0 critical warnings)
- frame_q[8:1][3] 是 Verilog-2001 非法写法(part-select 再取位), 综合报
  [Synth 8-2599] range is not allowed in a prefix 与 [Synth 8-1008] frame_q is not a memory
  -> 改为 frame_q[4] (= 数据字节 frame_q[8:1] 的 bit3)
- dx/dy 互换: 事件里把 dy 当 dx 上报
- 鼠标按键位错位: {2'b0, pkt_q[2], pkt_q[1]} 丢掉左键
  -> {2'b0, pkt_q[2], pkt_q[1], pkt_q[0]} (对齐 cpu21_mmio / mmio.h: bit0=左键, bit1=右键, bit2=中键)
- tools/check_rtl_text.ps1: 修 tag 变量引用解析错、修块注释漏报、字符串中文误报
- create_project.tcl 头注: Vivado 版本改为 2020.2 (本机实测: 建工程 + 综合均通过)
2026-09-18 16:16:53 +08:00
Alpha e4d58a3439 fix(saolei): 修复被编辑器覆盖的 board_top 端口行; 自检脚本支持 -Fix; tcl 默认定义 SAOLEI_ROM
- cpu21_board_top.v: PS/2 例化里 .ev_valid_o 被一行 README 正文覆盖, 综合报
  [Synth 8-2715] non-printable character 0xe4 / syntax error near ...
- tools/check_rtl_text.ps1: 修掉块注释导致的漏报(board_top 就是被漏掉的那个);
  改为按 UTF-8 原始字节读写(保持中文/换行/BOM 不变); 整行正文可用 -Fix 自动删除
- create_project.tcl: 新增 use_saolei 开关并定义宏 SAOLEI_ROM (1=扫雷, 0=走马灯)
2026-09-18 16:13:47 +08:00
Alpha df7b3aee6d fix(rtl): 清除被工具反复注入到 .v 末尾的 README 残行; 新增 RTL 文本自检脚本
- cpu21_pointer.v / cpu21_ps2_mouse.v / cpu21_vga.v 末尾多出一行 README 正文
  (Tcl ...), 会导致 Vivado: ERROR: [Synth 8-2715] non-printable character 0xe4
- 新增 pipelined-cpu/tools/check_rtl_text.ps1: 综合前秒级自检, 检出代码位置上
  的非 ASCII 文本/粘贴进来的正文行
2026-09-18 16:02:02 +08:00
Alpha 4e3525ffe2 fix(saolei): 修复被文本工具改坏的 RTL —— 这是综合失败的直接原因
## 现象(Vivado 2020.2 综合日志)

`synth_design -top cpu21_board_top -part xc7a100tcsg324-1` 报 13 个
`ERROR: [Synth 8-2715] syntax error`,指向:
- `cpu21_pointer.v:106`、`cpu21_board_top.v:308`、`cpu21_vga.v:242`
  —— `non-printable character with the hex value '0xe4'`(UTF-8 中文字的首字节)
- `cpu21_board_top.v:139` —— `syntax error near "="`
- `cpu21_ps2_mouse.v:45 / 47 / 145` —— `0xe3`、`syntax error near "ta_s1"`
- `cpu21_board_rom.vh:3...` —— 一连串 `syntax error near "="`(前一处错误的级联)

## 根因

**不是 RTL 设计问题,是工作区文件被改坏**(编辑器/格式化类的文本改动把 README 的
片段插进了 Verilog 文件里):

1. `cpu21_pointer.v`、`cpu21_vga.v`、`cpu21_ps2_mouse.v` 文件末尾被追加了一行
   来自 README 的 `Tcl 一键重建`(于是合成器在 `endmodule` 之后遇到中文字节);
2. `cpu21_board_top.v` 里被插入了同样的残行,**顶掉了 VGA 例化的 `.vga_hs` 那一行**;
3. `cpu21_board_top.v` 的 ROM 段被吞掉一整块:
   `reg [31:0] rom [...]` / `integer i;` / `initial begin` 全部消失,
   只剩下 `for (i = 0; ...) rom[i] = ...;` —— 这也正是 `cpu21_board_rom.vh`
   里 `syntax error near "="` 的由来(`initial` 块没了,`rom[i]=...` 成了非法语句);
4. `cpu21_ps2_mouse.v` 的 `reg ps2_clk_s1, ps2_clk_s2, ps2_data_s1, ps2_data_s2;`
   声明被拼进了 `assign ps2_data_oe_o = 1'b0` 里(变成
   `assign ps2_data_oe_o = 1'b02_data_s1, ps2_data_s2;`),注释残片
   `。本模块只接收,两条线都不驱动。` 夹在中间。

## 修复

- 删除 3 处 README 残行,恢复 `cpu21_board_top.v` 的 `.vga_hs` 端口连接;
- 补齐 `cpu21_board_top.v` ROM 段的 `reg/integer/initial begin`;
- 恢复 `cpu21_ps2_mouse.v` 被拼掉的 `reg` 声明,并整理开漏赋值的注释;
- 全量扫描 `srcs/**` 确认:无残留 README 文本碎片、`module/endmodule` 与
  `` `ifdef/`endif `` 配对正常、四个受损文件的关键结构与例化(ROM/RAM/CPU/VGA/
  数码管/按键、16 个字形表、PS2 状态机、按键去抖)均完整。

## 状态

- 本次只改 RTL 文本,未改任何逻辑/端口/地址。
- 本机没有 Vivado / xsim / iverilog,**无法在本地复验语法**,需要重新跑一次综合确认。
2026-09-18 15:58:13 +08:00
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
42 changed files with 18937 additions and 1650 deletions
+10
View File
@@ -4,6 +4,7 @@ pipelined-cpu/*
!pipelined-cpu/tools/
!pipelined-cpu/programs/
!pipelined-cpu/create_project.tcl
!pipelined-cpu/create_project_saolei.tcl
# 本地临时目录(仿真脚本、日志、中间文件)
_tmp/
@@ -14,3 +15,12 @@ _vivado/
*.pb
*.hex.lst
.Xil/
# 在仓库根目录批量跑 Vivado 时产生的日志/命令记录(pipelined-cpu/ 下已被上面的规则忽略)
vivado*.jou
vivado*.log
vivado*.backup.jou
vivado*.backup.log
# saolei 的构建产物(tools/build.ps1 一键重建,不入库)
saolei/build/
+792
View File
@@ -0,0 +1,792 @@
# `cpu21-riscv-4.circ` 的 Verilog 实现
处理器顶层文件:`pipelined-cpu/srcs/rtl/cpu21_riscv_redirect_int_bpb.v`
该设计对应原电路中的“重定向流水线+中断+分支预测”部分,顶层模块为
`cpu21_riscv_redirect_int_bpb`。为便于单独仿真与调试,各功能单元已拆分为
独立文件,顶层的 ALU、BPB 也已移出,具体见文末“文件与模块划分”。
## 目录结构
```text
risc-v/
├─ README.md 本文档
├─ pipelined-cpu/ Vivado 工程目录(也是源文件所在地)
│ ├─ create_project.tcl 一键重建工程的 Tcl 脚本
│ ├─ srcs/rtl/*.v 处理器与外设 RTL(18 个文件,cpu21_riscv_redirect_int_bpb.v 为处理器顶层)
│ │ 外设:cpu21_mmio.v(MMIO 中枢)、cpu21_vga.v(显示)、
│ │ cpu21_seg7.v(数码管)、cpu21_pointer.v(按键当鼠标)、
│ │ cpu21_ps2_mouse.v / cpu21_ps2_kbd.v(PS/2 鼠标、键盘接收)
│ ├─ srcs/board/*.v|*.vh 板级顶层(时钟分频 + 片内 ROM/RAM + 引脚);四份 ROM 初始化:
│ │ cpu21_board_rom.vh / _fast.vh(跑马灯)、risc-v-intrupt-ccab.vh
│ │ (课程中断综合测试程序)、cpu21_saolei_rom.vh(扫雷)
│ ├─ srcs/constrs/*.xdc 引脚与分频时钟约束(+ Nexys4DDR 母版)
│ ├─ srcs/tb/*.v 七个测试平台(含 PS/2 鼠标/键盘的单独自测 tb_ps2_mouse.v / tb_ps2_kbd.v)
│ ├─ srcs/rom/*.hex 测试平台加载的 ROM 镜像
│ ├─ tools/ rv_asm.py(迷你汇编器)、hex2vh.py(hex → ROM 初始化)、
│ │ adapt_intrupt_ccab.py(课程中断程序 → 上板 ROM)、
│ │ run_regress.ps1(一键回归)、check_rtl_text.ps1(RTL 文本体检)、
│ │ rars_f0c874c.jar(课程提供的 RARS 汇编器)、play_saolei.tcl(命令行玩扫雷)
│ └─ programs/ 汇编源程序与期望值:跑马灯 demo、指令集/MMIO 测试、课程原始
│ 资产(risc-v-benchmark_ccab、真值表),以及中断综合测试程序
│ risc-v-intrupt-ccab.asm / .hex
└─ saolei/ 纯 C 扫雷(MMIO 显示 + 中断读鼠标),见下文
├─ sw/ C 源码 + 链接脚本(三个固定中断入口)
└─ tools/ build.ps1(C → ROM)、check_isa.py(指令白名单校验)
```
**仓库里不放任何 Vivado 生成物**(`.xpr`、`.runs`、`.sim`、`.cache`、`.hw`、
`.ip_user_files`、`.Xil`、`vivado.jou/log` 本地已清理干净):需要工程时用
`source create_project.tcl` 一键重建,这样仓库始终是最清爽的源文件状态。
约定:git 只跟踪 `pipelined-cpu/` 下的源文件(`srcs/`、`tools/`、`programs/`、
`create_project.tcl`),Vivado 生成物(`*.xpr`、`*.runs`、`*.sim`、`*.cache`…)由
`.gitignore` 排除。`srcs/rtl/` 只放可综合的处理器源码;板级顶层与约束在 `srcs/board/`、
`srcs/constrs/`;测试平台与 ROM 镜像在 `srcs/tb/`、`srcs/rom/`(镜像是按 MMIO 写 LED
修改过的版本,课程原始程序与 RARS 输出保留在 `programs/`)。
## 已转换的功能
- 五级流水:IF、ID、EX、MEM、WB。
- EX/MEM 与 MEM/WB 前递,以及 load-use 一拍停顿。
- 条件分支 `beq`、`bne`、`blt`/`bge`(有符号)、`bltu`/`bgeu`(无符号)在 EX 段判定,错误预测时清空 IF/ID、ID/EX 并重定向 PC。真值表中 BLT 的 `ALU_OP` 为 SLT,与实现一致。
- `jal`、`jalr` 在 EX 段重定向,返回地址写回 `rd`。
- 8 项全相联 BPB:标签 `PC[11:2]`,每项包含 valid、目标 PC、2 位饱和计数器和 3 位年龄字段。BPB 只由条件分支更新,避免 `jal`、`jalr` 占用不会被查询的表项;未命中时优先使用无效项,否则替换年龄最大的项。
- 三路中断输入 `irq_i[2:0]`:两级同步、上升沿挂起、IRQ3 > IRQ2 > IRQ1 优先级,仅在 `ustatus[0]`(MIE)允许时响应。
- 支持嵌套中断:保存 EPC、`ustatus` 和当前优先级;`URET` 使用当前 `uepc` 返回,并从硬件栈恢复外层上下文。三个入口按照原电路的 `INTpc` 选择:IRQ1=`0x30ac`、IRQ2=`0x31e4`、IRQ3=`0x3310`。`0x22` 是 `ecall` 的服务号 34,不是中断向量。
- CSRRSI、CSRRCI,以及 CSRRW:支持 CSR 地址 `0x004`(`ustatus`)和 `0x041`(`uepc`)。立即数为 `IR[19:15]` 的零扩展值,并按 RISC-V 规则把旧 CSR 值写回 `rd`;这覆盖任务书中的 `csrrsi s6,0x41,0` 和 `csrrw zero,0x41,s6` 用法。
- `ecall`:停机和调试用。`a7 == 34` 是历史遗留的“显示”调用,现已保留为无操作;其余取值(例如程序结尾惯用的 `a7 = 10`)置 `halted_o`,直到 `go_i` 为高。LED 显示改由 MMIO 存储完成(见下文“MMIO 与外设”)。
- `sw` 和 `sb`:输出 `data_wstrb_o`,支持按字节写入。
- `lui`:加载高位立即数,`rd = imm[31:12] << 12`(按需求补充的扩展指令,标准 opcode `0x37`)。
- `sltiu`:I 型 `funct3=011`,立即数符号扩展后按无符号与 rs1 比较。
- M 扩展乘法(`funct7=0000001`):`mul`(乘积低 32 位)、`mulh`(有符号×有符号高 32 位)、`mulhsu`(有符号×无符号高 32 位)、`mulhu`(无符号×无符号高 32 位)。
- M 扩展除法:课程真值表里的 `divu`(`funct3=101`,无符号商)与 `remu`(`funct3=111`,无符号余数)。
以上六条乘法/除法指令都由 **多周期单元** `pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v` 完成(见“时序和使用说明”第 7 条),
而不是组合逻辑。
## 指令编码
表中的 `opcode` 是控制器使用的五位字段 `IR[6:2]`,不是包含最低两位的 7 位原始 opcode。
因此它与标准 RISC-V 指令的低 7 位 opcode 相差右移两位。
| 指令类别 | opcode | 说明 |
| ----------------------------------------------------------- | -------: | ----------------------------------------------------------------------------------------- |
| R 型 | `0x0c` | add/sub/and/or/xor/slt/sltu/sll/srl/sra |
| M 扩展(R 型) | `0x0c` | `funct7=0000001`:mul/mulh/mulhsu/mulhu、divu(`funct3=101`)、remu(`funct3=111`) |
| I 型 ALU | `0x04` | addi/andi/ori/xori/slti/sltiu/slli/srli/srai |
| `lui`(扩展) | `0x0d` | 加载高位立即数,`rd = imm[31:12] << 12` |
| `lw` | `0x00` | `funct3=010` |
| `sw` / `sb` | `0x08` | `funct3=010` / `000` |
| `beq` / `bne` / `blt` / `bge` / `bltu` / `bgeu` | `0x18` | `funct3=000` / `001` / `100` / `101` / `110` / `111` |
| `jal` | `0x1b` | J 型立即数 |
| `jalr` | `0x19` | `funct3=000` |
| 系统类 | `0x1c` | `ecall`、`URET`、CSRRSI、CSRRCI |
`ecall` 和 `URET` 都是 `opcode=0x1c、funct3=0`,由 `IR[21]` 区分:
`IR[21]=0` 是 `ecall`,`IR[21]=1` 是 `URET`。这一点来自原控制器中的 `IR21` 分支逻辑。
## MMIO 与外设
外设寄存器映射在数据存储器地址空间,由 `pipelined-cpu/srcs/rtl/cpu21_mmio.v` 在 MEM 级译码。
这些地址都在 4 KB 数据 RAM(`0x0000`–`0x0FFC`)之外,因此不会与 RAM 冲突。
每行的字节偏移都按**字**对齐译码(同一 32 位寄存器内任意字节偏移都命中)。
| 地址 | 外设 | 写 | 读 |
| --------------------------- | -------------- | ------------------------------------------------------------------------------------ | ---------------------------- |
| `0x0000_1000` | LED 数据寄存器 | `sw` 写全字;`sb` 按 `data_wstrb_o` 只改对应字节通道,`led_valid_o` 拉高一拍 | `lw` 返回 LED 寄存器当前值 |
| `0x0000_1004` | 七段数码管 | `sw`/`sb` 写 8 位十六进制数(低 4 位 = 最右边那个数码管) | `lw` 读回 |
| `0x0000_1100`+ | 鼠标寄存器组 | STATUS / DATA / ACK / CTRL / INPUT / TIME / FRAME / EVCNT / LEVEL / MAGIC | 同左(DATA 读无副作用) |
| `0x0000_1140`+ | 显示控制器 | CTRL(光标可见、背景色)/ CURSOR(列、行) | 同左(另含 MAGIC 自检值) |
| `0x0000_2000`–`0x2FFF` | 显存 | 40 列 × 30 行、每格 2 字节(低字节 = 字形[5:0]、高字节 = 颜色[2:0],共 2400 字节),写端口接到板级顶层的双口 RAM | `lw` 组合读回 |
其中 `LEVEL`(`0x1120`)是一个**软件中断**寄存器:写 1 会拉起 `irq_i[1]`,用来让程序
"跳到关卡入口";`MAGIC`(`0xCAFE0001`/`0xCAFE0002`)可以读来自检 MMIO 是否活着。
鼠标事件 FIFO(深度 16)配合 `mouse_irq_o` 提供"事件到达即中断",`isr_body` 读空 FIFO 后
中断线自动回 0,因此不会丢事件。
地址译码按**字**对齐:同一 32 位寄存器内的任意字节偏移(如 `0x1001`)都算命中,
具体修改哪几个字节由 `data_wstrb_o` 决定。`data_addr_o` 在命中 MMIO 时不会
访问外部 RAM;读数据由 `cpu21_mmio` 的输出在顶层与 `data_rdata_i` 做选择。
典型用法(LED 写 `0x12345678`,可用 `pipelined-cpu/tools/rv_asm.py` 汇编):
```asm
lui t0, 0x1 # t0 = LED_ADDR = 0x00001000
lui t1, 0x12345
addi t1, t1, 0x678 # t1 = 0x12345678
sw t1, 0(t0) # MMIO 写 LED -> LED 输出 12345678
lw a0, 0(t0) # MMIO 读回
```
`pipelined-cpu/programs/mmio_test.asm`(期望值见 `mmio_test.expected`)覆盖了
全字写、读回、字节写三种情况。加入新外设(拨码开关、UART 等)时只需在
`pipelined-cpu/srcs/rtl/cpu21_mmio.v` 里扩展地址译码,处理器流水线无需改动。
## 顶层端口约定
- `instr_i`:当前 `instr_addr_o` 对应的 32 位指令。原 Logisim 中由 ROM 提供。
- `data_rdata_i`:当前 `data_addr_o` 的读数据。原 Logisim 中由 MIPS RAM 提供。
- `data_addr_o`、`data_wdata_o`、`data_wstrb_o`、`data_we_o`:数据存储器接口。地址按字节地址输出;若使用原 32 位 RAM,可用 `data_addr_o[11:2]` 作为字地址。
- `irq_i[0]`、`irq_i[1]`、`irq_i[2]`:1、2、3 号中断源;输入应为脉冲或边沿信号,模块内部会同步并锁存挂起状态。
- `go_i`:对应原电路的“继续运行”按键。
- `if_pc_o`、`id_pc_o`、`ex_pc_o`、`mem_pc_o`、`wb_pc_o`:五级流水当前保存的 PC,气泡时输出 0,便于替代原电路的调试探针。
- `rdin_o`、`mdin_o`、`reg_write_o`、`mem_write_o`:对应原电路的寄存器写回和存储器写调试输出。
- `cycle_count_o`、`stall_count_o`、`bubble_count_o`、`conditional_taken_count_o`、`unconditional_branch_count_o`、`prediction_success_count_o`、`prediction_failure_count_o`:对应原电路和课程要求的周期、停顿/气泡、分支和动态预测统计量。
## 时序和使用说明
1. `reset` 为高时异步清空流水线、寄存器文件、BPB、挂起中断和中断栈,PC 置为 `RESET_PC`;`ustatus` 复位为参数 `USTATUS_INIT`,默认 `0x0000_0001`(MIE 上电即使能)。这样做的原因是课程的中断测试程序只在中断处理程序的“保护现场”之后写 MIE,主流程不会开中断;若不默认使能,第一次中断只会挂在 `irq_pending_o` 上而永不响应。参数可覆盖,例如把 `USTATUS_INIT` 设为 `32'h0` 即可恢复“上电关中断”的行为。
2. 指令存储器按组合读处理;数据存储器读数据在一个 MEM 周期内返回。若 RAM 为同步读,需要在外部增加一级数据寄存或相应调整 MEM/WB。
3. 响应中断时允许当前 EX 指令完成,并保存下一条尚未执行指令的 PC;若 EX 是已正确预测的跳转分支,则保存实际目标地址。EX 为空时优先保存 IF/ID 中尚未执行的 PC。这样 `URET` 不会重复执行已经提交的存储或寄存器写操作。
4. 默认 BPB 新表项采用弱不跳转/弱跳转状态:首次更新为不跳转时计数器为 `01`,首次更新为跳转时为 `10`;命中后按 2 位饱和规则增减。
5. `bpb_predict_hit_o`、`bpb_predict_taken_o`、`bpb_mispredict_o` 和 `irq_*_o` 是观测端口,不参与外部控制,可以直接接逻辑分析仪或测试平台。
6. `cycle_count_o` 在 `halted_o` 为高时暂停;load-use 每次计入一个气泡,重定向或中断清空 IF/ID 与当前 IF 时计入两个气泡。
7. 乘除法是多周期指令:结果算完之前 `ex_md_stall` 有效,ID/EX 保持(指令继续占用 EX)、EX/MEM 插气泡、取指/译码冻结,同时屏蔽中断(避免结果未定型就写回)。一次乘/除共占用 34 拍(1 拍装载 + 32 拍迭代 + 1 拍定型),这些等待周期也计入 `stall_count_o` / `bubble_count_o`。乘除法迭代期间不会把错误值写进 rd,也不会因为中断被打断。
原 `.circ` 文件中的 ROM、MIPS RAM、按钮、LED 和调试显示器属于 Logisim 外围,不直接搬入 RTL;其功能分别由指令接口、数据接口、`go_i`、MMIO(`led_data_o`/`led_valid_o`)和调试端口替代。
## 文件与模块划分
| 文件 | 模块 | 职责 |
| ----------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------- |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_redirect_int_bpb.v` | `cpu21_riscv_redirect_int_bpb` | 顶层:五级流水寄存器、子模块例化、PC/IF-ID/ID-EX 控制、对外输出 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_alu.v` | `cpu21_riscv_alu` | 组合 ALU(加减/移位/逻辑/比较,不做乘除) |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v` | `cpu21_riscv_muldiv` | 多周期乘除法单元:32 拍移位迭代,mul/mulh/mulhsu/mulhu/divu/remu |
| `pipelined-cpu/srcs/rtl/cpu21_bpb_8.v` | `cpu21_bpb_8` | 8 项全相联分支目标缓冲(2 位饱和计数器 + 近似 LRU) |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_decoder.v` | `cpu21_riscv_decoder` | ID 级组合译码器(操作码`IR[6:2]`) |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_regfile.v` | `cpu21_riscv_regfile` | 寄存器堆,`x0` 恒为 0,同周期 WB 写优先旁路 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_forward_unit.v` | `cpu21_riscv_forward_unit` | EX 级前递网络(EX/MEM 优先于 MEM/WB) |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_store_unit.v` | `cpu21_riscv_store_unit` | EX 级存储数据对齐:`sw`/`sb` 的写数据与字节写掩码 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_branch_unit.v` | `cpu21_riscv_branch_unit` | EX 级控制流裁决:分支方向/目标、JAL/JALR 重定向 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_hazard_unit.v` | `cpu21_riscv_hazard_unit` | load-use 冒险、停顿(stall)与冲刷(flush)判定 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_irq_ctrl.v` | `cpu21_riscv_irq_ctrl` | 中断采样与优先级、`ustatus`/`uepc` CSR、嵌套返回栈 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_perf_counters.v` | `cpu21_riscv_perf_counters` | 周期/停顿/气泡/分支/预测成功率等统计计数器 |
| `pipelined-cpu/srcs/rtl/cpu21_mmio.v` | `cpu21_mmio` | MMIO 中枢:LED、七段数码管、鼠标寄存器组 + 事件 FIFO + 中断、显示控制器、显存窗口 |
| `pipelined-cpu/srcs/rtl/cpu21_vga.v` | `cpu21_vga` | 640x480@60 显示控制器:按"格子"扫描,8x8 字模放大 2 倍填满 16x16 的格子 |
| `pipelined-cpu/srcs/rtl/cpu21_seg7.v` | `cpu21_seg7` | 板载 8 位数码管动态扫描驱动(共阳极,位选/段选低有效) |
| `pipelined-cpu/srcs/rtl/cpu21_pointer.v` | `cpu21_pointer` | 把 5 向按键变成"鼠标"事件(去抖 + 事件排队,单位 1/8 格) |
| `pipelined-cpu/srcs/rtl/cpu21_ps2_mouse.v` | `cpu21_ps2_mouse` | 可选的真鼠标:PS/2 接收(11 位帧按位下标对齐 + **奇校验** + 3 字节包解析),只实现接收 |
| `pipelined-cpu/srcs/rtl/cpu21_ps2_kbd.v` | `cpu21_ps2_kbd` | 可选的真键盘:扫描码集 2 解码(`F0` 断码 / `E0` 扩展前缀),把按键映射成与鼠标同格式的事件 |
| `pipelined-cpu/srcs/board/cpu21_board_top.v` | `cpu21_board_top` | 板级顶层:时钟分频、复位同步、片内 ROM/RAM、显存双口 RAM、VGA/数码管、PS/2 键鼠选择(SW15)、中断连线 |
| `pipelined-cpu/srcs/board/cpu21_saolei_rom.vh` | — | 扫雷程序的 ROM 初始化(由`saolei/tools/build.ps1` 生成) |
| `pipelined-cpu/srcs/constrs/cpu21_board_top.xdc` | — | Nexys4DDR 引脚约束 + 分频时钟(`create_generated_clock`) |
| `pipelined-cpu/srcs/board/cpu21_board_rom.vh` | — | 片内 ROM 内容(生成文件,`rom[i] = 32'h…;`) |
| `pipelined-cpu/srcs/tb/tb_board_top.v` | `tb_board_top` | 板级顶层的 RTL 仿真(观察 LED 跑马灯与写入指示) |
| `pipelined-cpu/srcs/tb/tb_no_intr.v` | `tb_no_intr` | 无中断测试平台(指令集/MMIO 测试,默认加载 benchmark 镜像) |
| `pipelined-cpu/srcs/tb/tb_cpu21_riscv_redirect_int_bpb.v` | `tb_cpu21_riscv_redirect_int_bpb` | 中断与分支预测测试平台(默认加载中断测试镜像) |
| `pipelined-cpu/srcs/tb/tb_saolei.v` | `tb_saolei` | 扫雷自动打完一局并 dump 出显存(配 `saolei/tools/fb2png.py` 渲染成 PNG) |
| `pipelined-cpu/srcs/tb/tb_saolei_play.v` | `tb_saolei_play` | 命令行“边玩边截图”的测试平台(配 `tools/play_saolei.tcl`) |
| `pipelined-cpu/srcs/tb/tb_ps2_mouse.v` | `tb_ps2_mouse` | PS/2 鼠标接收器自测:奇校验、3 字节包、按键位(几秒出结果) |
| `pipelined-cpu/srcs/tb/tb_ps2_kbd.v` | `tb_ps2_kbd` | PS/2 键盘解码自测:方向键 / Enter / 左 Shift / 松开码不产生事件 |
| `pipelined-cpu/tools/rv_asm.py` | — | 迷你汇编器:`.asm` → 机器码 hex(`.lst` 带反汇编清单) |
| `pipelined-cpu/tools/hex2vh.py` | — | hex →`pipelined-cpu/srcs/board/cpu21_board_rom.vh`(`rom[i] = 32'h…;` 初始化语句) |
拆分只调整了模块边界与文件组织,端口、时序与行为完全等价:使用同一 ROM 程序
在 xsim 下对拆分前后逐周期对比,波形输出与最终统计(周期、停顿、气泡、分支
跳转、预测成败)完全一致。
## 各模块的实现方式
下面按“处理器流水线 → 外设 → 板级”的顺序说明每个模块的实现要点(对应上面
“文件与模块划分”表)。全部是可综合的纯 RTL,没有例化 Xilinx 原语,
因此 `xvlog`/`xelab` 可以直接仿真。
### 流水线主体
- **`cpu21_riscv_redirect_int_bpb`(顶层)**:五级寄存器 IF/ID、ID/EX、EX/MEM、
MEM/WB 各自带 valid 位;下一个 PC 由一个多路选择器决定(复位 `RESET_PC`、
顺序 PC+4、分支/跳转重定向目标、`URET` 返回地址、中断入口)。停顿与冲刷分别
由 hazard unit 的 `pipeline_stall_o`、`flush_younger_o` 控制:停顿时冻结
PC/IF-ID 并给 ID/EX 插气泡;冲刷时只把年轻指令的 valid 清零,所以已经提交的
存储/寄存器写不会被执行两次。
- **`cpu21_riscv_alu`**:纯组合,4 位 `alu_op` 直接选择加减/逻辑/移位/比较;
减法用“B 取反加 1”实现,比较分有符号/无符号两条路径。乘除法不在 ALU 里
(历史上的组合 `a*b`/`a%b` 是关键路径,已拆到多周期单元)。
- **`cpu21_riscv_muldiv`**:32 拍移位迭代(乘法 shift-add、除法 restoring),
加装载与定型共占用 EX **34 拍**;期间 `ex_md_stall` 有效 → 保持 ID/EX、给
EX/MEM 插气泡、冻结取指、屏蔽中断(见“时序和使用说明”第 7 条)。接口沿用
ALU 约定:`result_o` 是低位/商、`result2_o` 是高位/余数;除零时商全 1、
余数取被除数。
- **`cpu21_riscv_decoder`**:ID 级组合译码,操作码取 `IR[6:2]`,`funct3`/
`funct7`/`IR[21]` 区分 R 型细分、M 扩展、CSR 与 `ecall`/`URET`;输出一组
控制位(`alu_op`、`alu_src`、立即数、`reg_write`、访存使能、`wb_sel`、
分支/跳转、CSR 操作、`md_op`)。
- **`cpu21_riscv_regfile`**:32×32 寄存器堆,`x0` 恒 0;两个组合读口 + 一个写口,
同周期 WB 的数据通过“写优先旁路”直送到读口,省掉一拍。
- **`cpu21_riscv_forward_unit`**:EX 级前递,把 EX/MEM 与 MEM/WB 的结果和 ID/EX
的 `rs1/rs2` 比较,EX/MEM 优先;EX/MEM 是 load 时不参与前递(load 结果要
到 MEM 才有效,改由 hazard unit 停顿一拍);store 的数据同样可以前递。
- **`cpu21_riscv_store_unit`**:EX 级数据对齐,`sw` 产生四个字节通道、`sb` 按
`addr[1:0]` 只打开一个通道,并把数据搬到对应字节位置,输出 `data_wstrb_o`。
- **`cpu21_riscv_branch_unit`**:EX 级裁决分支方向与目标:6 条条件分支各用有
符号/无符号比较,`jal`/`jalr` 总是重定向,并把 `PC+4` 写回 `rd`(链接地址
在 EX 级预先算好放进 EX/MEM,避免在写回/前递路径上再加一个加法器)。
- **`cpu21_riscv_hazard_unit`**:load-use 冒险(ID/EX 是 load 且和 ID 级要用的
寄存器相同)插一个气泡;`flush_younger` 的优先级高于停顿:中断、`URET`、
`ecall` 停机、分支/跳转重定向都会丢弃年轻指令,此时不再插气泡。
- **`cpu21_riscv_irq_ctrl`**:`irq_i` 先打两拍同步,再与“上一拍”比较取出
**上升沿**(挂起位锁存),优先级 IRQ3 > IRQ2 > IRQ1;响应条件是“有挂起 +
`ustatus[0]`(MIE) + 嵌套深度 < 4 + 未停机 + EX 级没有重定向/停机”。
`ustatus`(0x004) 与 `uepc`(0x041) 用组合逻辑实现 CSRRS/CSRRC/CSRRW 三种读写
语义(旧值回写 `rd`);嵌套栈保存 EPC / `ustatus` / 上一层优先级,`URET` 时
逐层恢复。响应时保存“下一条尚未执行的指令的 PC”,EX 是已正确预测的跳转
分支则保存实际目标,避免 `URET` 重复执行已提交的操作。
- **`cpu21_riscv_perf_counters`**:周期、停顿、气泡、条件分支、无条件跳转、
预测成功/失败六个计数器;`halted_o` 期间不计周期;load-use 记 1 个气泡,
重定向/中断清空记 2 个气泡。
### 外设
- **`cpu21_mmio`**:MEM 级译码;先用**字对齐**地址比较命中哪个寄存器(这样
`sb` 写到 `0x1001` 也能命中 LED 寄存器),再用 `data_wstrb_o` 决定改哪几个
字节;命中时给出 `rdata_hit_o`,顶层据此在 RAM 与 MMIO 之间选择读数据。
内部包含 LED/数码管寄存器、鼠标寄存器组(含 16 深事件 FIFO 与 `mouse_irq_o`)、
显示控制器、显存窗口,以及用于自检的 `MAGIC` 常量。
- **`cpu21_vga`**:工作在 CPU 时钟下,用 `tick`(每 2 拍一个使能)产生 25 MHz
像素节拍(**不引入第二个时钟域**);按 640×480@60 的行场计数产生同步信号,
并按“格子”寻址显存:一个显存字对应 4 个像素,8×8 字模放大 2 倍填满 16×16
的格子(40×30 格),帧末产生 `frame_tick_o`。
- **`cpu21_seg7`**:8 位动态扫描。计数到 `REFRESH_DIV` 换一位(默认 50 MHz 下
50000 → 每步 1 ms,每位数码管 125 Hz 刷新);十六进制→七段码用查表;共阳极
所以位选/段选都取反输出,`an_en_i` 可以把某几位熄灭。
- **`cpu21_pointer`**:把 5 向按键变成鼠标事件。用约 1 ms 的采样节拍对两级同步
后的按键采样,只在“按下”边沿产生事件;一次采样可能同时按下多个键,用待发
队列 + 优先级选择器逐个上报(每拍最多一个事件,不丢键)。事件单位是 1/8 格,
软件端只用整数加减就能换算成格(不需要除法)。
- **`cpu21_ps2_mouse`**:可选的真鼠标,只实现接收:`ps2_clk` 下降沿把 11 位帧
**按位的下标**存进 `frame_q`(`frame_q[i]` = 第 i 位,所以字节 = `frame_q[8:1]`、
校验 = `frame_q[9]`、停止 = `frame_q[10]`),停下来后判“停止位 = 1 + 奇校验”,
再按 3 字节包解析出 dx/dy/按键,产生 `ev_valid_o` 脉冲。这里两个细节曾经写错过,
导致真设备的数据**全被丢掉**(上板表现为 `rx_count` 恒定 0):
① 原来用整体右移 `{data, frame_q[10:1]}`,判 `frame_q[10]` 时停止位还没移进来,
判到的是校验位,字节也整体错了一位;② 原来按**偶**校验判,而 PS/2 是**奇**校验。
- **`cpu21_ps2_kbd`**:可选的真键盘,扫描码集 2:`0xE0` = 扩展键前缀、`0xF0` = 松开
前缀,只有**按下**才产生事件(松开码丢弃),所以按住方向键时键盘自己的重复码正好
变成“连续移动”。产出的事件格式与鼠标**完全一致**(↑/↓/←/→ = dy/dx ±8,
一格;Enter/空格 = 左键;左 Shift/F/C = 右键),因此 `saolei.c`/`isr.c` 一行都不用改。
### 板级
- **`cpu21_board_top`**:把这些接起来,只引出板上真正需要的引脚。内含参数化分频器
(`CLK_DIV` / `CLK_DIV_INTRUPT`)、复位同步器(异步置位 + 同步释放)、片内
ROM(组合读、`nop` 填充、`ifdef` 链选程序)与 RAM(字节写使能 + 组合读、
只响应 4 KB 以内地址,避免和 MMIO 别名)、显存双口 RAM(CPU 写口 + VGA 读口
同处一个时钟域,用像素使能而不是第二时钟)、数码管、VGA、PS/2 键鼠(同一根
PS/2 线上同时挂鼠标与键盘两个解码器,用 `hid_sel`(SW15) 选哪个生效,板载 5 向
按键始终有效)、中断连线(SW0/1/2 分别与鼠标事件/关卡软件中断相或)、
锁存式中断指示 LED(请求上升沿置位、对应级别的 ISR 执行 `uret` 后熄灭)和
`led_we` 写入指示;另有一个“PS/2 诊断”模式把收到的字节数/时钟活动/线电平
摆到 LED 高位(`DEBUG_PS2_RX_LED`,见下文“PS/2 键鼠”)。
## 上板(Nexys4DDR + Vivado)
处理器顶层 `cpu21_riscv_redirect_int_bpb` 有 775 位调试端口(各级 PC/IR、计数器、
BPB/中断观测值)。如果直接把它设为实现顶层,这些端口会全部变成引脚,导致
`[Place 30-415] IO Placement failed due to overutilization`(占用 775 个 IO,
而 `xc7a100tcsg324-1` 只有 210 个)。因此上板时用 `cpu21_board_top` 作为顶层,
只引出板上真正需要的那些引脚(LED、按键、拨码、VGA、8 位数码管、可选的 PS/2),
其余调试端口不连接。定义宏 `SAOLEI_ROM` 时还会把扫雷程序固化进 ROM;当前
`create_project.tcl` 默认定义的是 `RISC_V_INTRUPT_CCAB_ROM`(跑课程的
中断综合测试程序),宏的含义、优先级和改法见下文“内置程序与宏的用法”。
### 引脚分配(`cpu21_board_top.xdc`)
| 端口 | 引脚 | 板上元件 | 说明 |
| --------------- | ----------- | ------------------------- | ----------------------------------------------------------------------- |
| `clk100` | E3 | 100 MHz 晶振 | 经`CPU_DIV` 分频后给 CPU(默认 50 MHz) |
| `rst_n` | C12 | 按键 CPU_RESETN(低有效) | 异步置位、与 CPU 时钟同步释放 |
| `go` | N17 | 按键 BTNC | 对应原电路“继续运行”(`ecall` 停机后恢复执行) |
| `irq_sw[2:0]` | J15/L16/M13 | 拨码开关 SW0–SW2 | 三路中断源,拨动即触发(内部同步并锁存) |
| `hid_sel` | V10 | 拨码开关 SW15 | 选 PS/2 设备:0 = 键盘(默认),1 = 鼠标(见下文“PS/2 键鼠”) |
| `sw_flag` | R15 | 拨码开关 SW3 | 扫雷的插旗模式(软件读 INPUT 寄存器;自动鼠标右键同样能插旗) |
| `led[15:0]` | H17…V11 | LED0–LED15 | `LED[2:0]` = 三路中断指示(锁存,ISR 返回后熄灭);`LED[15:3]` 平时熄灭,`DEBUG_PS2_RX_LED=1` 时是 PS/2 诊断(见下文) |
| `led_we` | R12 | LED16_B | MMIO 写入指示,每写一次拉长约 0.34 s @50 MHz |
### PS/2 键鼠(板载 USB HID 口,也可改接 Pmod JA)
Nexys4DDR 右上角那个 **USB HOST** 口不是给 FPGA 直连的 USB:板上有一颗 USB↔PS/2
桥接芯片,它把自己当 USB 主机,把键盘/鼠标当 PS/2 设备,输出的是 PS/2 的两根线
(官方母版标明 `ps2_clk = F4`、`ps2_data = B2`)。所以 FPGA 侧只需要一个 PS/2
接收器,**不用写 USB 协议栈**。
| 接线方式 | 能不能用 | 说明 |
| ------------------------------------- | -------- | ---------------------------------------------------------------- |
| 键鼠用**有线模式**插 USB HOST | 最推荐 | Type-C/PS2-USB 转换都用它的有线模式 |
| 2.4G USB 接收器 | 可以试 | 部分复合设备桥接芯片不转发 |
| 蓝牙 | 不行 | 桥接芯片不支持蓝牙,键盘的蓝牙链路它根本看不见 |
| Pmod PS/2(JA)+ 真 PS/2 键鼠 | 保底 | 直接进 FPGA,只需把 XDC 改回 `ps2_clk = C17`、`ps2_data = D18` |
**键盘/鼠标不动时一个字节都不发**,所以插上不动时诊断 LED 不跳是正常的:要一边按
键/移鼠标一边看。
**选设备:SW15 拨下(0)= PS/2 键盘,拨上(1)= PS/2 鼠标**(默认键盘)。两个
解码器挂在同一根线上,但只有选中的那一路事件会送给 CPU —— 不能让两路同时生效:
键盘的扫描码会凑巧组成“合法的鼠标包”,会让光标乱跳。
诊断 LED(`DEBUG_PS2_RX_LED = 1`,默认开):
| LED | 含义 |
| ------------------ | ---------------------------------------------------------- |
| `LED[15:6]` | 当前选中设备**收到的字节数**低 10 位(按键时数字跳 = 通了) |
| `LED[5]` | `ps2_clk` 有边沿活动(时钟在动就快速翻转) |
| `LED[4]` / `LED[3]` | `ps2_data` / `ps2_clk` 当前电平(静态,判断线有没有被拉死) |
| `LED[2:0]` | 三路中断指示(和其它模式一样) |
排查口诀:**LED[5] 在跳但字节数不动** = 桥接芯片在发数据、我们的解码没认(帧格式/
校验不匹配,先怀疑奇偶校验);**LED[5] 也不跳** = 桥接芯片根本没转发这个设备
(换有线模式,或改走 Pmod JA)。
| 操作 | 键盘 | 鼠标 |
| ---------------- | --------------------------------- | ---------- |
| 移动光标 | `↑` `↓` `←` `→` | 移动 |
| 左键(开始/开格) | `Enter`、空格 | 左键 |
| 右键(插旗) | 左键 `Shift`、`F`、`C` | 右键 |
按住方向键 = 连续移动(用的是键盘自己的重复码)。**C 程序一行都不用改**:键盘产出的
事件与 `cpu21_pointer`、`cpu21_ps2_mouse` 格式完全相同(±8 = 一格),一样从 IRQ2
进 `saolei/sw/isr.c`。不想要 PS/2 就把顶层参数 `USE_PS2_MOUSE` 设为 0(只剩板载
5 向按键);PS/2 接收器的帧格式/校验有专门的几秒钟自测(见下文“回归测试”)。
两条**上板实测**出来的方向约定(都值得写进报告):
- **鼠标的 dy 要取负**:PS/2 的原生约定是「正 dx = 右、**正 dy = 鼠标上移**」,
而软件里「正 dx = 列 +1、**正 dy = 行 +1(向下)**」(板载按键 BTND 就是 +8),
所以 `cpu21_ps2_mouse.v` 在拼包时把 dy 取负、dx 不动。不定这个负号的表现就是
**左右正常、上下反向** —— 不是解码错。
- **键鼠静止时一个字节都不发**,所以“插上不动没反应”是正常的,必须一边按键/
移鼠标一边看 LED。
### 时钟:分频比怎么选(`CLK_DIV` / `CLK_DIV_INTRUPT`)
乘除法原本是 ALU 里的组合运算(`a*b`、`a%b`),实测关键路径约 94 ns(325 级逻辑、
286 个 CARRY4),在 100 MHz 下必然不收敛,所以之前只能把时钟 16 分频到 6.25 MHz。
现在乘除法改成了 `pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v` 里的 **32 拍迭代多周期单元**,关键路径上
不再有组合乘/除,也不需要再分频了;但把所有加法器/比较器都换成迭代后,在 100 MHz
下仍有约 1.1 ns 的违例——那是 **EX 级“分支判决 / 中断保存 → BPB 更新”这条与乘除法
无关的组合路径**(逻辑只占约 3 ns,剩下 7~8 ns 全是布线)。
因此在 `cpu21_board_top` 里保留一个参数化的分频器 `CLK_DIV`,默认取 **2(50 MHz,
周期 20 ns)**,既比原来的 16 分频快 8 倍,又留出充足余量:
| 配置 | CPU 时钟 | 实测 WNS(xc7a100tcsg324-1) | 说明 |
| -------------- | -------- | ---------------------------- | ------------------------------------- |
| `CLK_DIV=1` | 100 MHz | -1.1 ns ✗ | 违例路径与乘除法无关(分支/中断/BPB) |
| `CLK_DIV=2` | 50 MHz | **+5.1 ns** ✓ | 默认,余量约 25% |
| `CLK_DIV=16` | 6.25 MHz | +6.1 ns ✓ | 改造前的设置,现在没必要了 |
分频后的时钟在综合/实现时由 Vivado 自动插入 BUFG 走全局时钟网络(RTL 里不例化
`BUFG` 原语,这样在不认识 Xilinx 原语的仿真器里也能直接仿真)。XDC 中必须用
`create_generated_clock` 声明这个分频时钟,否则时序分析会按 10 ns 去要求 CPU
内部路径而误报大量违例:
```tcl
create_generated_clock -name cpu_clk -source [get_ports { clk100 }] \
-divide_by 2 [get_pins { clk_div_reg/Q }]
```
改分频比时:**变慢**(分频比变大)不用动 XDC;**变快**(分频比小于 2)就要把 XDC 的
`-divide_by` 一起改小,否则约束会比实际时钟松、可能掩盖违例。
### 中断测试程序为什么还要再慢一档
标着 `RISC_V_INTRUPT_CCAB_ROM` 的课程中断综合测试程序里,两次显示之间的延时常量
是按 50 MHz 写的:50 MHz 下每个数值只停留约 10 ms,动态扫描的数码管上根本读不
出来。所以顶层在跑这个程序时改用**另一个分频比参数 `CLK_DIV_INTRUPT`**:停留时间
与分频比成正比:取 8 → 12.5 MHz(约 40 ms/值)、取 32 → 3.125 MHz(约 160 ms/值)——
当前默认 32,在 `cpu21_board_top.v` 里改。这个分频比只在定义了
`RISC_V_INTRUPT_CCAB_ROM` 时生效,其它程序仍用 `CLK_DIV`。
### 分频与刷新率一览(想改速度看这里)
| 想改什么 | 改哪里 | 当前值 / 换算 |
| -------------------------- | --------------------------------------------------- | ------------------------------------------------------------ |
| CPU 主频(程序快慢) | 参数`CLK_DIV` | 2 → 50 MHz(`100 MHz / CLK_DIV`) |
| 中断程序的 CPU 主频 | 参数`CLK_DIV_INTRUPT`(只在该 ROM 宏下生效) | 32 → 3.125 MHz;每个数值约 10 ms × (`CLK_DIV_INTRUPT`/2) |
| 数码管扫描刷新率 | 顶层`SEG_REFRESH_DIV`(自动跟随 `CPU_DIV`) | `100000 / CPU_DIV` → 每步 1 ms、每位 125 Hz |
| LED16_B 写入指示点亮时长 | `led_we_cnt` 的装载值 `24'hFF_FFFF` | 约 0.34 s @50 MHz(`0xFFFFFF / f_cpu`) |
| 中断程序每个数值的停留时间 | `tools/adapt_intrupt_ccab.py` 的 `DELAY_NEG_HI` | `0xFFFC0` → 约 10 ms @50 MHz |
- **数码管**:`SEG_REFRESH_DIV = 100000 / CPU_DIV` 保证“每步 1 ms、每位数码管
125 Hz”与主频无关(不这样缩放的话,主频降 4 倍时全屏刷新只剩 31 Hz,能看到
闪烁)。想更亮可以把它改小(扫描更快),但每位刷新率要远高于 50 Hz。
`cpu21_seg7` 的分频计数是 20 位,最大约 100 万,所以 100 MHz(不分频)也放得下。
- **慢下来的副作用**:`cpu21_pointer` 的按键采样节拍(`SAMPLE_DIV = 50000`)也会
跟着变慢(3.125 MHz 下约 16 ms 采样一次,仍在可接受范围);`cpu21_vga` 的
25 MHz 像素节拍是从 CPU 时钟分频出来的,主频一降 VGA 行场时序就超出规范
(中断测试程序不用 VGA,无影响)。
- **XDC 不用跟着改(只要不是变快)**:`cpu_clk` 固定按最坏情况 `-divide_by 2`
(50 MHz)约束,实际时钟更慢属于“约束偏严”,不会掩盖违例;只有把分频比改到
比 2 更小(更快)时,才需要同步改小 XDC 的 `-divide_by`。
### 片内 ROM / RAM
- ROM:`ROM_WORDS = 8192` 字(32 KB),组合读,取指地址 `instr_addr[ROM_AW+1:2]`;
内容由 `` `include `` 一个 ROM 初始化文件给出(与 `cpu21_board_top.v` 同目录,
文件里是 `rom[i] = 32'h…;` 语句,放在 `initial` 里,等价于 `$readmemh`,
且不依赖仿真工作目录)。具体包含哪份由宏决定(见下文“内置程序与宏的用法”)。
超出镜像长度的地址填 `nop`(`32'h0000_0013`),所以跳飞时不会执行到随机指令。
- RAM:`RAM_WORDS = 1024` 字(4 KB),字节写使能 + 组合读,只响应
`data_addr < 4 KB`,因此 MMIO 地址(默认 `0x1000`)不会落进 RAM。
- 两个 `initial` 块都用 NOP(`32'h0000_0013`)或 0 预填充,ROM 未使用区域是 NOP,
这样即使程序跑飞也不会执行到随机指令而乱写寄存器。
### 内置程序与宏的用法
板级顶层用一条 `` `ifdef `` 链决定 ROM 里固化哪个程序(连带决定中断入口地址),
`create_project.tcl` 里的两个开关就是给这条链用的:
| 宏(`sources_1` 的 Verilog Define) | ROM 文件 | 中断入口`IRQ1/2/3` | 说明 |
| ------------------------------------- | -------------------------------------- | --------------------------------- | -------------------------------------------- |
| `SAOLEI_ROM`(优先级最高) | `srcs/board/cpu21_saolei_rom.vh` | `0x100` / `0x200` / `0x300` | 纯 C 扫雷(`saolei/tools/build.ps1` 生成) |
| `RISC_V_INTRUPT_CCAB_ROM` | `srcs/board/risc-v-intrupt-ccab.vh` | `0x0AC` / `0x1E4` / `0x308` | 课程中断综合测试程序(当前默认) |
| `BOARD_ROM_FAST` | `srcs/board/cpu21_board_rom_fast.vh` | `0x100` / `0x200` / `0x300` | 仿真专用:跑马灯延时改小(综合不用) |
| 都不定义 | `srcs/board/cpu21_board_rom.vh` | `0x100` / `0x200` / `0x300` | 上板跑马灯 demo(默认) |
同一时间只应定义一个(`ifdef` 链按上表顺序取第一个命中的)。**中断入口必须和
ROM 里 ISR 的起点一致**,所以换程序时宏与入口是一起换的:`cpu21_board_top.v`
里 `.IRQ1/2/3_VECTOR` 也跟着 `RISC_V_INTRUPT_CCAB_ROM` 分支走。
#### 怎么定义宏
1. **Vivado GUI**:`Project` → `Settings` → `Synthesis` → `Verilog Options` →
`Verilog Macros` 里填 `RISC_V_INTRUPT_CCAB_ROM`(只对 `sources_1` 有效),
确定后 `Reset Synthesis Run` 再综合。也可以直接在 Tcl Console 里写:
```tcl
set_property verilog_define {RISC_V_INTRUPT_CCAB_ROM} [get_filesets sources_1]
reset_run synth_1
launch_runs synth_1
wait_on_run synth_1
```
换回跑马灯就是 `set_property verilog_define {} [get_filesets sources_1]`。
2. **一键重建**:改 `create_project.tcl` 里的 `use_intrupt_ccab` / `use_saolei`
两个开关(同一时间只开一个),重新 `source create_project.tcl`。
3. **命令行仿真**:`xvlog` 与 `xelab` 都要带同一个 `-d`,并用 `-i` 指向头文件目录:
```powershell
xvlog -d RISC_V_INTRUPT_CCAB_ROM -i srcs/board `
srcs/rtl/*.v srcs/board/cpu21_board_top.v 你的tb.v
xelab -top 你的tb -snapshot s -d RISC_V_INTRUPT_CCAB_ROM
```
4. **注意**:这些宏只影响“综合用”的 `sources_1`。跑 `tb_no_intr` /
`tb_cpu21_riscv_redirect_int_bpb` 时,程序来自测试平台自己探测的
`srcs/rom/*.hex`,与宏无关。
#### 中断综合测试程序的适配(`tools/adapt_intrupt_ccab.py`)
课程给的 `programs/risc-v-intrupt-ccab.hex` 不能直接上板,脚本把它适配成
`srcs/board/risc-v-intrupt-ccab.vh`,且**不移动任何指令地址**(三个中断入口
仍是 `0x0AC` / `0x1E4` / `0x308`):
```powershell
python tools/adapt_intrupt_ccab.py programs/risc-v-intrupt-ccab.hex `
srcs/board/risc-v-intrupt-ccab.vh _tmp/fast.hex _tmp/plain.hex
```
六处适配(脚本内每步都有断言,输入不对会直接报错):
1. 三个 ISR 里的 `addi a7,zero,10` + `ecall` → 两条 `nop`(`a7 != 34` 的
`ecall` 会让这颗 CPU 停机等 `go_i`);
2. 11 个显示点 `lui a7,1` + `sw a0,0(a7)` → `jal` 调用追加的显示例程;
顺带纠正早期手改留下的 `sb` 误码(只写低字节会显示错值);
3. 追加主程序用的 `DISP_DLY`(0x438):写 LED 寄存器(数码管显示它)+ 约 10 ms
延时;返回地址存在 `sp+256`,位于 ISR 压栈区(最多 192 B)之上,所以中断
随时打断延时也不会破坏它;
4. 修正 ISR2/ISR3 出栈顺序写乱的 12 条 `lw`(否则 ISR2 的循环计数会变成
-3823,看起来像“二级中断返回之后卡住”);
5. ISR3 的 SB 测试改用 ISR3 自己保存的 `s4/s5/s6`(原来用未保存的 `t1/t3/s2`,
抢占 ISR1 时会毁掉 ISR1 的 DIVU 循环计数,造成死循环);
6. 追加 ISR 专用的 `DISP_DLY_ISR`(0x458):**链接寄存器改用 `a0`、显示值走
`a7`**(这两个寄存器三个 ISR 都保存/恢复),于是 ISR 完全不碰 `ra`。否则
中断若恰好落在主程序 `DISP_DLY` 末尾的 `jalr` 上,返回会跳进 ISR 代码、
最后执行到 ISR 结尾的 `csrrci` 把 MIE 清零(“跑一阵子之后拨开关不再响应”);
第 3 个参数生成“延时极小”的仿真版,便于在 xsim 里跑完整流程;第 4 个参数生成
只去掉停机 `ecall`、显示仍是 `lui`+`sw` 的“原样”版本,用于对比。
适配后的 ROM 在仿真里验证控制流很简单:用 `-d RISC_V_INTRUPT_CCAB_ROM` 编译
`srcs/rtl/*.v` + `srcs/board/cpu21_board_top.v` 加一个自建的 tb(或直接用
`srcs/tb/tb_board_top.v` 看 LED),观察 `irq_current_level_o`(0→1→2→3→…→0)、
`uepc_o`、`halted_o` 是否依次回到 0,以及 `LED[2:0]` 是否随嵌套逐层点亮/熄灭。
建议用第 3 个参数生成的快版 ROM(否则一次中断就是几十毫秒仿真时间)。
### 演示程序的生成与仿真
```powershell
# 在工程目录 pipelined-cpu/ 下执行:
# 1) 汇编 + 生成 ROM 初始化文件
python tools/rv_asm.py programs/board_demo.asm _tmp/board_demo.hex
python tools/hex2vh.py _tmp/board_demo.hex srcs/board/cpu21_board_rom.vh 1024
# 2) 上板顶层的 RTL 仿真(默认程序延时约 20 ms,只需确认第一次写入 LED=0001)
xvlog srcs/rtl/*.v srcs/board/cpu21_board_top.v srcs/tb/tb_board_top.v
xelab -top tb_board_top -snapshot board_sim
xsim board_sim -tclbatch run_quit.tcl
```
`board_demo.asm` 是一段 16 步跑马灯(`0001 → 0002 → … → 8000 → 0001`),
每步之间用软件循环延时约 20 ms(50 MHz 下)。仓库里同时提供了两份 ROM 初始化
文件,由 `BOARD_ROM_FAST` 宏选择(综合流程不会定义该宏):
| 文件 | 用途 | 每步耗时 |
| ---------------------------------------------------- | ------------------------------ | -------- |
| `pipelined-cpu/srcs/board/cpu21_board_rom.vh` | 综合/上板(默认) | 约 20 ms |
| `pipelined-cpu/srcs/board/cpu21_board_rom_fast.vh` | 仅仿真:一次跑完整个跑马灯序列 | 2.6 µs |
```powershell
# 快速版仿真:编译时加上 -d BOARD_ROM_FAST(xvlog 与 xelab 都要带)
xvlog -d BOARD_ROM_FAST srcs/rtl/*.v srcs/board/cpu21_board_top.v srcs/tb/tb_board_top.v
xelab -top tb_board_top -snapshot board_sim_fast -d BOARD_ROM_FAST
```
两份 ROM 初始化文件由 `programs/board_demo.asm`(默认,≈ 20 ms/步)与
`programs/board_demo_fast.asm`(快速版,≈ 2.6 µs/步,只差延时循环的初值)生成:
```powershell
python tools/rv_asm.py programs/board_demo_fast.asm _tmp/board_demo_fast.hex
python tools/hex2vh.py _tmp/board_demo_fast.hex srcs/board/cpu21_board_rom_fast.vh 1024
```
### Vivado 工程设置与实现结果
Vivado 工程 `pipelined-cpu/pipelined-cpu.xpr` 被 `.gitignore` 排除,需要手动设置:
1. `sources_1` 顶层设为 `cpu21_board_top`(纯 RTL 仿真时也可切回
`cpu21_riscv_redirect_int_bpb`);
2. 加入 `pipelined-cpu/srcs/board/cpu21_board_top.v`、`pipelined-cpu/srcs/constrs/cpu21_board_top.xdc`,
四份 ROM 初始化文件(`cpu21_board_rom.vh`、`cpu21_board_rom_fast.vh`、
`risc-v-intrupt-ccab.vh`、`cpu21_saolei_rom.vh`)的文件类型都设成 `Verilog Header`
(不能当普通源码,否则会被当成顶层候选);
3. 三个 testbench 放在**仿真 fileset**(`sim_1` 顶层 `tb_cpu21_riscv_redirect_int_bpb`、
`sim_2` 顶层 `tb_no_intr`),不参与综合,因此不会再出现“testbench 被当成顶层”
导致的 `[Place 30-494] The design is empty`。
`xc7a100tcsg324-1` 上的实现结果(乘除法改为多周期单元、`CLK_DIV=2` = 50 MHz):
| 项目 | 结果 |
| -------- | -------------------------------------------------------------------------------------------------- |
| 综合 | `synth_design Complete!` |
| 实现 | `route_design Complete!`、`write_bitstream Complete!`,`WNS = +5.109 ns`(无 Failed Timing) |
| 资源 | LUT 3420 (5.39%)、FF 2507、**DSP 0**(组合乘法器已消失)、IOB 23 (10.95%) |
| bit 文件 | `pipelined-cpu/pipelined-cpu.runs/impl_1/cpu21_board_top.bit` |
对比改造前(组合乘除法 + `CLK_DIV=16`):LUT 5400 → 3420、DSP 12 → 0、
CPU 时钟 6.25 MHz → 50 MHz(快 8 倍),并且乘除法不再位于关键路径上。
上板操作:用 Vivado 打开工程 → `Open Hardware Manager` → `Program Device`,选上述
`.bit`。复位后先跑 `sources_1` 里宏选中的那个程序。以跑马灯为例:复位后立即从
`0001` 开始逐位左移(演示程序不执行 `ecall`,所以不用按 `go`;`go` 只对执行了
`ecall` 停机、等待继续运行的程序有意义),LED16_B 每次写入时点亮。
#### 跑中断综合测试程序时的现象
(`create_project.tcl` 默认就是它,无需额外设置)
- 复位后主程序先跑自己的指令测试,数码管上显示程序写进 `LED_ADDR` 的 32 位十六进制值
(低 4 位在最右边那个数码管);
- 拨动 **SW0 / SW1 / SW2**(J15/L16/M13)就对应 1/2/3 号中断:数码管随即切成该 ISR 的
显示内容(SB 读写测试、DIVU 测试、多层循环等),每个数值停留约 160 ms(3.125 MHz 下);
- `LED[2:0]` 是**锁存**的中断指示:从该路中断请求出现开始亮,一直到它的 ISR 执行完
`uret` 返回才熄灭。抢占(例如 ISR1 还没跑完就拨 SW2)不会把外层的灯灭掉,一级
一级返回时才依次熄灭,正好能看出嵌套层次;
- LED16_B 每次 MMIO 写入内一下;程序里若有 `ecall a7 != 34` 的停机点,会停在原地等
按 **BTNC**(`go`)继续;
- 想重新开始随时按 **CPU_RESETN**(`rst_n`);想让它更快/更慢就改 `CLK_DIV_INTRUPT`
(见上文“分频与刷新率一览”)。
## 扫雷程序(`saolei/`)
`saolei/` 是一个**纯 C 写的扫雷**,用 RISC-V 工具链编译成机器码后固化进片内 ROM 运行,
反过来验证这套 MMIO/中断/显示/鼠标外设。完整说明见 `saolei/README.md`,要点:
- **三段中断入口**(地址由 `saolei/sw/linker.ld` 定死,与顶层 `IRQ1/2/3_VECTOR` 成对):
IRQ1=`0x100` 关卡(游戏)程序、IRQ2=`0x200` 鼠标读取、IRQ3=`0x300` 备用;
- **两层结构**:背景 `main()` 画开始界面并一直运行 → 点"开始"写 `LEVEL` 寄存器拉起软件中断
→ CPU 跳进 `0x100` 的关卡程序跑完整局 → 点左上角黄格 `uret` 回到背景被打断的那条指令;
- **纯 C**,只有三处必要的内联汇编:设 `sp`、三个中断入口的现场保护、`uret`;
- **指令子集适配**:没有 `auipc`/`lb`/`lbu`/有符号 `div`,所以程序只按字节写显存、
索引全用移位、且不含 `.data`/`.rodata`(链接脚本用 `ASSERT` 兜住);
- **构建**:`saolei/tools/build.ps1`(C → ELF → `.hex` + `cpu21_saolei_rom.vh`),
并用 `check_isa.py` 逐条反汇编检查有没有用到未实现的指令。实测 842 条指令全部合法;
- **上板切换**:Vivado 里定义宏 `SAOLEI_ROM` 就加载扫雷程序,不定义仍是下面的跑马灯。
### 扫雷:怎么烧到板上
```powershell
# 1) 生成扫雷的 ROM 镜像(改了 saolei/sw/*.c 之后必须重跑这一步)
cd saolei
powershell -ExecutionPolicy Bypass -File tools\build.ps1 `
-Prefix 'C:\Users\29653\tools\riscv\xpack-riscv-none-elf-gcc-15.2.0-1\bin\riscv-none-elf-'
# 2) 一键建"扫雷专用"工程(先清掉旧的同名工程再重建;sources_1 和 sim_1 都固定定义 SAOLEI_ROM)
cd ..\pipelined-cpu
& 'C:\Xilinx\Vivado\2020.2\bin\vivado.bat' -mode batch -source create_project_saolei.tcl -log create_saolei.log
# 产出 pipelined-cpu-saolei.xpr(和通用工程 pipelined-cpu 互不影响)
```
然后在 Vivado GUI 里依次点:`Run Synthesis` → `Run Implementation` → `Generate Bitstream`
→ `Open Hardware Manager` → `Open Target / Auto Connect` → `Program Device`。
板上操作:
| 板子上 | 作用 |
| ------------------------- | ---------------------------------------------------------- |
| BTNU / BTND / BTNL / BTNR | 光标 上 / 下 / 左 / 右 |
| BTNC | 左键:开始界面确认 / 关卡里开格 |
| SW3 拨上 + BTNC | 插旗 / 取消旗 |
| SW15 = 1 | 改用 PS/2 鼠标(默认 SW15 = 0 = PS/2 键盘,见上文“PS/2 键鼠”) |
| 键盘 `↑↓←→` / `Enter`·空格 / 左`Shift`·`F`·`C` | 移动 / 左键 / 右键(插旗)—— 需 SW15 = 0 |
| 鼠标 移动 / 左键 / 右键 | 移动 / 左键 / 右键(插旗)—— 需 SW15 = 1 |
| SW0 / SW1 / SW2 | **别动**,它们会 OR 进外部中断线,一拨就可能触发中断 |
开始界面的光标默认停在 START 按钮上,按 BTNC 进关卡;关卡里把光标移到盘面**左上角那个黄格**
再按 BTNC 就退回开始界面(分出胜负后随便点一下也能退)。
### 扫雷:没有 VGA 显示器怎么玩(命令行仿真 + 每步截图)
显存是内存映射的(`0x2000` 起,40×30 格、每格 2 字节:低字节 = 字形[5:0]、
高字节 = 颜色[2:0],共 2400 字节 = 600 字),
所以仿真里可以把整屏 dump 出来,再按 `cpu21_vga.v` 的字库/调色板还原成 PNG —— 等价于给 VGA 截图。
`tools/play_saolei.tcl` 更进一步:用 `cmd.txt` 往仿真里"喂"按键,每按一步自动存一张 PNG。
```powershell
# ① 建仿真快照(只在第一次,或改了 RTL 之后需要重跑)
cd D:\Desktop\risc-v-cpu
New-Item -ItemType Directory -Force -Path _tmp\saolei_sim | Out-Null # 用哪个空目录都行
cd _tmp\saolei_sim
$p = 'D:\Desktop\risc-v-cpu\pipelined-cpu'
$rtl = (Get-ChildItem "$p\srcs\rtl\*.v").FullName
& 'C:\Xilinx\Vivado\2020.2\bin\xvlog.bat' -d SAOLEI_ROM -i "$p\srcs\board" -i "$p\srcs\rtl" $rtl "$p\srcs\board\cpu21_board_top.v" "$p\srcs\tb\tb_saolei_play.v"
& 'C:\Xilinx\Vivado\2020.2\bin\xelab.bat' -d SAOLEI_ROM -top tb_saolei_play -snapshot saolei_play
# ② 开始玩(之后是交互式的,命令直接敲在 xsim 提示符下)
& 'C:\Xilinx\Vivado\2020.2\bin\xsim.bat' saolei_play
```
到 `xsim%` 提示符后(**`source` 要带 `-encoding utf-8`**,否则 xsim 按 GBK 读脚本会乱码):
```text
source -encoding utf-8 D:/Desktop/risc-v-cpu/pipelined-cpu/tools/play_saolei.tcl
k c 左键点击(进关卡用这个)
k u/d/l/r 光标 上/下/左/右(也认 w/s/a/d)
k f 插旗模式点击 k p 只切换插旗开关
k z 复位 k uucc 连写多个动作(上、上、点、点)
shot 重画 + 存图 view 用看图器打开最新那张图
help / quit
```
- **每按一个动作都会自动存一张 PNG**:`shots/001_c.png`、`shots/002_u.png`… 可以直接贴进报告;
同时会覆盖 `shots/latest.png` —— 在 VS Code 里打开这一张,每操作一步它自动刷新,等于边玩边看屏幕;
- 仿真速度约 1 ms 仿真时间 = 0.5 s 墙钟,所以每一步等 1~2 秒;
- 只想"看一局"不想手动操作:用 `srcs/tb/tb_saolei.v`(`-top tb_saolei`)会自动打完一整局
(首击展开 → 全盘扫描 → 216/216 通关),并 dump 出 `fb_00_start.txt` … `fb_13_after_sweep.txt`,
再用 `py saolei\tools\fb2png.py <dump.txt> <out.png>` 渲染成 640×480 PNG;
- 光标格在画面里是**黄色 2 像素常亮方框**,VGA 上和截图里都能一眼看到指针在哪。
## 回归测试
工程里带了一个 xsim 回归脚本,在 `pipelined-cpu/` 目录下运行:
```powershell
powershell -ExecutionPolicy Bypass -File tools\run_regress.ps1
```
它会按顺序做四件事:
1. 编译 `srcs/rtl/` 下的处理器源码;
2. 跑 4 个指令测试(`muldiv_test`、`isa_test`、`lui_test`、`mmio_test`),把测试平台
打印的 LED 序列与 `programs/*.expected` 逐值比对;
3. 跑两个流水基线(`tb_no_intr`、`tb_cpu21_riscv_redirect_int_bpb`),打印 FINAL 统计行;
4. 跑板级顶层(`-d BOARD_ROM_FAST`),检查跑马灯 LED 序列。
最后汇总 PASS/FAIL,并以失败数作为退出码。编译与仿真的中间文件都写在
`%TEMP%\cpu21_regress` 下,不会污染工程目录;需要 `xvlog`/`xelab`/`xsim` 在 PATH 上。
期望结果(2026-09,多周期乘除法 + `CLK_DIV=2`):
| 测试 | 期望 |
| -------------------------------------------------- | ------------------------------------------------------------------------------ |
| muldiv_test / isa_test / lui_test / mmio_test | 13 / 24 / 4 / 5 个 LED 值全部匹配 |
| tb_no_intr(benchmark,5560 周期) | `stalls=1528 bubbles=1830 cond_taken=639 uncond=71 pred_ok=687 pred_fail=80` |
| tb_cpu21_riscv_redirect_int_bpb(中断,6003 周期) | `stalls=963 bubbles=1205 cond_taken=547 uncond=28 pred_ok=557 pred_fail=87` |
| 板级(快速 ROM) | 跑马灯`0001 → … → 8000` 循环,每步约 2.6 µs |
PS/2 接收器不在上面这个脚本里(它跟板载芯片/真设备有关,不属于纯 RTL 回归),
但解码逻辑(帧对齐、奇校验、包解析、键位映射)值得单独盯,两个自测各几秒钟:
```powershell
cd pipelined-cpu
$p = 'D:\Desktop\risc-v-cpu\pipelined-cpu'
# 鼠标接收器
& 'C:\Xilinx\Vivado\2020.2\bin\xvlog.bat' "$p\srcs\rtl\cpu21_ps2_mouse.v" "$p\srcs\tb\tb_ps2_mouse.v"
& 'C:\Xilinx\Vivado\2020.2\bin\xelab.bat' -top tb_ps2_mouse -snapshot ps2m
& 'C:\Xilinx\Vivado\2020.2\bin\xsim.bat' ps2m -R # 期望 ALL PASS
# 键盘解码(把 mouse 换成 kbd)
& 'C:\Xilinx\Vivado\2020.2\bin\xvlog.bat' "$p\srcs\rtl\cpu21_ps2_kbd.v" "$p\srcs\tb\tb_ps2_kbd.v"
& 'C:\Xilinx\Vivado\2020.2\bin\xelab.bat' -top tb_ps2_kbd -snapshot ps2k
& 'C:\Xilinx\Vivado\2020.2\bin\xsim.bat' ps2k -R # 期望 ALL PASS
```
两个自测都会:发几个帧 → 检查事件内容是否正确;**故意发一个校验位错的帧** →
必须整帧丢掉(不计数、不产生事件);键盘那个还会检查“松开码(F0 xx)不产生事件”。
## 工程分享(Tcl 一键重建)
`pipelined-cpu/` 已整理成“自包含 + 可直接分享”的工程目录,设计文件都在里面,
别人拿到整个文件夹后**不需要改任何路径**:
```text
pipelined-cpu/
├─ create_project.tcl 一键重建脚本(用 [info script] 定位自身目录)
├─ srcs/rtl/*.v 处理器与外设 RTL(17 个文件)
├─ srcs/board/*.v|*.vh 板级顶层、四份 ROM 初始化(跑马灯/中断测试/扫雷/仿真快版)
├─ srcs/constrs/*.xdc 引脚约束 + 板级母版
├─ srcs/tb/*.v 三个测试平台
├─ srcs/rom/*.hex 测试平台加载的 ROM 镜像
├─ tools/、programs/ 迷你汇编器与测试程序(重生成 ROM 用)
├─ cpu21_riscv_redirect_int_bpb说明.md
└─ pipelined-cpu.xpr(+ *.runs/…) 本机工程,分享时可删
```
用法(在 Vivado Tcl Shell 里,或命令行 `vivado -mode batch -source create_project.tcl`):
```tcl
cd <pipelined-cpu 所在目录>
source create_project.tcl
```
脚本会就地创建 `pipelined-cpu.xpr`,并配好:
- `sources_1` 顶层 `cpu21_board_top`(处理器 RTL + 板级顶层 + 片内 ROM 初始化,
`cpu21_board_rom.vh` 标记为 `Verilog Header`);
- `constrs_1`:`cpu21_board_top.xdc`(引脚 + `CLK_DIV` 分频时钟)与板级母版;
- `sim_1`(顶层 `tb_cpu21_riscv_redirect_int_bpb`)与 `sim_2`(顶层 `tb_no_intr`);
测试平台会依次探测 ROM 镜像,其中 `../../../../../srcs/rom/` 对应 GUI 仿真的
工作目录(`<proj>.sim/sim_1/behav/xsim`),所以创建完就能直接 `Run Simulation`;
- 两个仿真 fileset 的 `xsim.simulate.runtime` 设为 `all`,避免 GUI 仿真只跑 1000 ns
就停下(看起来像卡死)。
分享前建议删掉生成目录:`*.xpr`、`*.runs`、`*.sim`、`*.cache`、`*.hw`、`*.gen`、
`*.ip_user_files`、`.Xil`。实测只带 `create_project.tcl` + `srcs/` + `pipelined-cpu/tools/` +
`programs/` + 说明文档约 **0.3 MB**,在任意路径重建工程并综合成功。
> 源文件只有一份,就在 `pipelined-cpu/srcs/`(`tools/`、`programs/` 同理),
> 直接在工程目录里改即可,没有需要同步的副本。
也可以用 Vivado 自动生成:
```tcl
write_project_tcl -force -paths_relative_to <工程目录> <工程目录>/create_project.tcl
```
注意自动生成的版本会把源路径写成**生成时的绝对路径**、并且要求 `origin_dir` 指向
工程目录,所以要手动改两行才能给别人用:
```tcl
set origin_dir [file dirname [file normalize [info script]]]
create_project ${_xil_proj_name_} $origin_dir -part xc7a100tcsg324-1
```
-363
View File
@@ -1,363 +0,0 @@
# `cpu21-riscv-4.circ` 的 Verilog 实现
处理器顶层文件:`pipelined-cpu/srcs/rtl/cpu21_riscv_redirect_int_bpb.v`
该设计对应原电路中的“重定向流水线+中断+分支预测”部分,顶层模块为
`cpu21_riscv_redirect_int_bpb`。为便于单独仿真与调试,各功能单元已拆分为
独立文件,顶层的 ALU、BPB 也已移出,具体见文末“文件与模块划分”。
## 目录结构
```text
risc-v/
├─ pipelined-cpu/ Vivado 工程目录(也是源文件所在地)
│ ├─ create_project.tcl 一键重建工程的 Tcl 脚本
│ ├─ srcs/rtl/*.v 处理器 RTL(13 个文件,cpu21_riscv_redirect_int_bpb.v 为顶层)
│ ├─ srcs/board/*.v|*.vh 板级顶层(时钟分频 + 片内 ROM/RAM + 引脚)、片内 ROM 初始化
│ ├─ srcs/constrs/*.xdc 引脚与分频时钟约束(+ Nexys4DDR 母版)
│ ├─ srcs/tb/*.v 三个测试平台
│ ├─ srcs/rom/*.hex 测试平台加载的 ROM 镜像
│ ├─ tools/ rv_asm.py(迷你汇编器)、hex2vh.py(hex → ROM 初始化)、run_regress.ps1(一键回归)
│ ├─ programs/ 汇编源程序、期望值、课程原始资产(RARS)
│ └─ pipelined-cpu.xpr(+*.runs/…) 本机工程与生成物(不入库,可删)
├─ cpu21_riscv_redirect_int_bpb说明.md 本文档
└─ _tmp/、_vivado/ 本地临时目录(不入库)
```
约定:git 只跟踪 `pipelined-cpu/` 下的源文件(`srcs/`、`tools/`、`programs/`、
`create_project.tcl`),Vivado 生成物(`*.xpr`、`*.runs`、`*.sim`、`*.cache`…)由
`.gitignore` 排除。`srcs/rtl/` 只放可综合的处理器源码;板级顶层与约束在 `srcs/board/`、
`srcs/constrs/`;测试平台与 ROM 镜像在 `srcs/tb/`、`srcs/rom/`(镜像是按 MMIO 写 LED
修改过的版本,课程原始程序与 RARS 输出保留在 `programs/`)。
## 已转换的功能
- 五级流水:IF、ID、EX、MEM、WB。
- EX/MEM 与 MEM/WB 前递,以及 load-use 一拍停顿。
- 条件分支 `beq`、`bne`、`blt`/`bge`(有符号)、`bltu`/`bgeu`(无符号)在 EX 段判定,错误预测时清空 IF/ID、ID/EX 并重定向 PC。真值表中 BLT 的 `ALU_OP` 为 SLT,与实现一致。
- `jal`、`jalr` 在 EX 段重定向,返回地址写回 `rd`。
- 8 项全相联 BPB:标签 `PC[11:2]`,每项包含 valid、目标 PC、2 位饱和计数器和 3 位年龄字段。BPB 只由条件分支更新,避免 `jal`、`jalr` 占用不会被查询的表项;未命中时优先使用无效项,否则替换年龄最大的项。
- 三路中断输入 `irq_i[2:0]`:两级同步、上升沿挂起、IRQ3 > IRQ2 > IRQ1 优先级,仅在 `ustatus[0]`(MIE)允许时响应。
- 支持嵌套中断:保存 EPC、`ustatus` 和当前优先级;`URET` 使用当前 `uepc` 返回,并从硬件栈恢复外层上下文。三个入口按照原电路的 `INTpc` 选择:IRQ1=`0x30ac`、IRQ2=`0x31e4`、IRQ3=`0x3310`。`0x22` 是 `ecall` 的服务号 34,不是中断向量。
- CSRRSI、CSRRCI,以及 CSRRW:支持 CSR 地址 `0x004`(`ustatus`)和 `0x041`(`uepc`)。立即数为 `IR[19:15]` 的零扩展值,并按 RISC-V 规则把旧 CSR 值写回 `rd`;这覆盖任务书中的 `csrrsi s6,0x41,0` 和 `csrrw zero,0x41,s6` 用法。
- `ecall`:停机和调试用。`a7 == 34` 是历史遗留的“显示”调用,现已保留为无操作;其余取值(例如程序结尾惯用的 `a7 = 10`)置 `halted_o`,直到 `go_i` 为高。LED 显示改由 MMIO 存储完成(见下文“MMIO 与外设”)。
- `sw` 和 `sb`:输出 `data_wstrb_o`,支持按字节写入。
- `lui`:加载高位立即数,`rd = imm[31:12] << 12`(按需求补充的扩展指令,标准 opcode `0x37`)。
- `sltiu`:I 型 `funct3=011`,立即数符号扩展后按无符号与 rs1 比较。
- M 扩展乘法(`funct7=0000001`):`mul`(乘积低 32 位)、`mulh`(有符号×有符号高 32 位)、`mulhsu`(有符号×无符号高 32 位)、`mulhu`(无符号×无符号高 32 位)。
- M 扩展除法:课程真值表里的 `divu`(`funct3=101`,无符号商)与 `remu`(`funct3=111`,无符号余数)。
以上六条乘法/除法指令都由 **多周期单元** `pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v` 完成(见“时序和使用说明”第 7 条),
而不是组合逻辑。
## 指令编码
表中的 `opcode` 是控制器使用的五位字段 `IR[6:2]`,不是包含最低两位的 7 位原始 opcode。
因此它与标准 RISC-V 指令的低 7 位 opcode 相差右移两位。
| 指令类别 | opcode | 说明 |
| ----------------------------------------------- | -----: | ----------------------------------------------------------------------------------- |
| R 型 | `0x0c` | add/sub/and/or/xor/slt/sltu/sll/srl/sra |
| M 扩展(R 型) | `0x0c` | `funct7=0000001`:mul/mulh/mulhsu/mulhu、divu(`funct3=101`)、remu(`funct3=111`) |
| I 型 ALU | `0x04` | addi/andi/ori/xori/slti/sltiu/slli/srli/srai |
| `lui`(扩展) | `0x0d` | 加载高位立即数,`rd = imm[31:12] << 12` |
| `lw` | `0x00` | `funct3=010` |
| `sw` / `sb` | `0x08` | `funct3=010` / `000` |
| `beq` / `bne` / `blt` / `bge` / `bltu` / `bgeu` | `0x18` | `funct3=000` / `001` / `100` / `101` / `110` / `111` |
| `jal` | `0x1b` | J 型立即数 |
| `jalr` | `0x19` | `funct3=000` |
| 系统类 | `0x1c` | `ecall`、`URET`、CSRRSI、CSRRCI |
`ecall` 和 `URET` 都是 `opcode=0x1c、funct3=0`,由 `IR[21]` 区分:
`IR[21]=0` 是 `ecall`,`IR[21]=1` 是 `URET`。这一点来自原控制器中的 `IR21` 分支逻辑。
## MMIO 与外设
外设寄存器映射在数据存储器地址空间,由 `pipelined-cpu/srcs/rtl/cpu21_mmio.v` 在 MEM 级译码,
顶层参数 `LED_ADDR`(默认 `0x0000_1000`)指定 LED 数据寄存器的地址。
该地址位于 4 KB 数据 RAM(`0x0000`–`0x0FFC`)之外,因此不会与 RAM 冲突。
| 地址 | 外设 | 写 | 读 |
| ------------- | -------------- | ---------------------------------------------------------------------------- | -------------------------- |
| `0x0000_1000` | LED 数据寄存器 | `sw` 写全字;`sb` 按 `data_wstrb_o` 只改对应字节通道,`led_valid_o` 拉高一拍 | `lw` 返回 LED 寄存器当前值 |
地址译码按**字**对齐:同一 32 位寄存器内的任意字节偏移(如 `0x1001`)都算命中,
具体修改哪几个字节由 `data_wstrb_o` 决定。`data_addr_o` 在命中 MMIO 时不会
访问外部 RAM;读数据由 `cpu21_mmio` 的输出在顶层与 `data_rdata_i` 做选择。
典型用法(LED 写 `0x12345678`,可用 `pipelined-cpu/tools/rv_asm.py` 汇编):
```asm
lui t0, 0x1 # t0 = LED_ADDR = 0x00001000
lui t1, 0x12345
addi t1, t1, 0x678 # t1 = 0x12345678
sw t1, 0(t0) # MMIO 写 LED -> LED 输出 12345678
lw a0, 0(t0) # MMIO 读回
```
`pipelined-cpu/programs/mmio_test.asm`(期望值见 `mmio_test.expected`)覆盖了
全字写、读回、字节写三种情况。加入新外设(拨码开关、UART 等)时只需在
`pipelined-cpu/srcs/rtl/cpu21_mmio.v` 里扩展地址译码,处理器流水线无需改动。
## 顶层端口约定
- `instr_i`:当前 `instr_addr_o` 对应的 32 位指令。原 Logisim 中由 ROM 提供。
- `data_rdata_i`:当前 `data_addr_o` 的读数据。原 Logisim 中由 MIPS RAM 提供。
- `data_addr_o`、`data_wdata_o`、`data_wstrb_o`、`data_we_o`:数据存储器接口。地址按字节地址输出;若使用原 32 位 RAM,可用 `data_addr_o[11:2]` 作为字地址。
- `irq_i[0]`、`irq_i[1]`、`irq_i[2]`:1、2、3 号中断源;输入应为脉冲或边沿信号,模块内部会同步并锁存挂起状态。
- `go_i`:对应原电路的“继续运行”按键。
- `if_pc_o`、`id_pc_o`、`ex_pc_o`、`mem_pc_o`、`wb_pc_o`:五级流水当前保存的 PC,气泡时输出 0,便于替代原电路的调试探针。
- `rdin_o`、`mdin_o`、`reg_write_o`、`mem_write_o`:对应原电路的寄存器写回和存储器写调试输出。
- `cycle_count_o`、`stall_count_o`、`bubble_count_o`、`conditional_taken_count_o`、`unconditional_branch_count_o`、`prediction_success_count_o`、`prediction_failure_count_o`:对应原电路和课程要求的周期、停顿/气泡、分支和动态预测统计量。
## 时序和使用说明
1. `reset` 为高时异步清空流水线、寄存器文件、BPB、挂起中断和中断栈,PC 置为 `RESET_PC`;`ustatus` 复位为参数 `USTATUS_INIT`,默认 `0x0000_0001`(MIE 上电即使能)。这样做的原因是课程的中断测试程序只在中断处理程序的“保护现场”之后写 MIE,主流程不会开中断;若不默认使能,第一次中断只会挂在 `irq_pending_o` 上而永不响应。参数可覆盖,例如把 `USTATUS_INIT` 设为 `32'h0` 即可恢复“上电关中断”的行为。
2. 指令存储器按组合读处理;数据存储器读数据在一个 MEM 周期内返回。若 RAM 为同步读,需要在外部增加一级数据寄存或相应调整 MEM/WB。
3. 响应中断时允许当前 EX 指令完成,并保存下一条尚未执行指令的 PC;若 EX 是已正确预测的跳转分支,则保存实际目标地址。EX 为空时优先保存 IF/ID 中尚未执行的 PC。这样 `URET` 不会重复执行已经提交的存储或寄存器写操作。
4. 默认 BPB 新表项采用弱不跳转/弱跳转状态:首次更新为不跳转时计数器为 `01`,首次更新为跳转时为 `10`;命中后按 2 位饱和规则增减。
5. `bpb_predict_hit_o`、`bpb_predict_taken_o`、`bpb_mispredict_o` 和 `irq_*_o` 是观测端口,不参与外部控制,可以直接接逻辑分析仪或测试平台。
6. `cycle_count_o` 在 `halted_o` 为高时暂停;load-use 每次计入一个气泡,重定向或中断清空 IF/ID 与当前 IF 时计入两个气泡。
7. 乘除法是多周期指令:结果算完之前 `ex_md_stall` 有效,ID/EX 保持(指令继续占用 EX)、EX/MEM 插气泡、取指/译码冻结,同时屏蔽中断(避免结果未定型就写回)。一次乘/除共占用 34 拍(1 拍装载 + 32 拍迭代 + 1 拍定型),这些等待周期也计入 `stall_count_o` / `bubble_count_o`。乘除法迭代期间不会把错误值写进 rd,也不会因为中断被打断。
原 `.circ` 文件中的 ROM、MIPS RAM、按钮、LED 和调试显示器属于 Logisim 外围,不直接搬入 RTL;其功能分别由指令接口、数据接口、`go_i`、MMIO(`led_data_o`/`led_valid_o`)和调试端口替代。
## 文件与模块划分
| 文件 | 模块 | 职责 |
| --------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------- |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_redirect_int_bpb.v` | `cpu21_riscv_redirect_int_bpb` | 顶层:五级流水寄存器、子模块例化、PC/IF-ID/ID-EX 控制、对外输出 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_alu.v` | `cpu21_riscv_alu` | 组合 ALU(加减/移位/逻辑/比较,不做乘除) |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v` | `cpu21_riscv_muldiv` | 多周期乘除法单元:32 拍移位迭代,mul/mulh/mulhsu/mulhu/divu/remu |
| `pipelined-cpu/srcs/rtl/cpu21_bpb_8.v` | `cpu21_bpb_8` | 8 项全相联分支目标缓冲(2 位饱和计数器 + 近似 LRU) |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_decoder.v` | `cpu21_riscv_decoder` | ID 级组合译码器(操作码`IR[6:2]`) |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_regfile.v` | `cpu21_riscv_regfile` | 寄存器堆,`x0` 恒为 0,同周期 WB 写优先旁路 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_forward_unit.v` | `cpu21_riscv_forward_unit` | EX 级前递网络(EX/MEM 优先于 MEM/WB) |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_store_unit.v` | `cpu21_riscv_store_unit` | EX 级存储数据对齐:`sw`/`sb` 的写数据与字节写掩码 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_branch_unit.v` | `cpu21_riscv_branch_unit` | EX 级控制流裁决:分支方向/目标、JAL/JALR 重定向 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_hazard_unit.v` | `cpu21_riscv_hazard_unit` | load-use 冒险、停顿(stall)与冲刷(flush)判定 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_irq_ctrl.v` | `cpu21_riscv_irq_ctrl` | 中断采样与优先级、`ustatus`/`uepc` CSR、嵌套返回栈 |
| `pipelined-cpu/srcs/rtl/cpu21_riscv_perf_counters.v` | `cpu21_riscv_perf_counters` | 周期/停顿/气泡/分支/预测成功率等统计计数器 |
| `pipelined-cpu/srcs/rtl/cpu21_mmio.v` | `cpu21_mmio` | MMIO 外设:LED 数据寄存器(可写、可读回,支持字节写) |
| `pipelined-cpu/srcs/board/cpu21_board_top.v` | `cpu21_board_top` | 板级顶层:时钟分频、复位同步、片内 ROM/RAM、引脚外设连接 |
| `pipelined-cpu/srcs/constrs/cpu21_board_top.xdc` | — | Nexys4DDR 引脚约束 + 分频时钟(`create_generated_clock`) |
| `pipelined-cpu/srcs/board/cpu21_board_rom.vh` | — | 片内 ROM 内容(生成文件,`rom[i] = 32'h…;`) |
| `pipelined-cpu/srcs/tb/tb_board_top.v` | `tb_board_top` | 板级顶层的 RTL 仿真(观察 LED 跑马灯与写入指示) |
| `pipelined-cpu/srcs/tb/tb_no_intr.v` | `tb_no_intr` | 无中断测试平台(指令集/MMIO 测试,默认加载 benchmark 镜像) |
| `pipelined-cpu/srcs/tb/tb_cpu21_riscv_redirect_int_bpb.v` | `tb_cpu21_riscv_redirect_int_bpb` | 中断与分支预测测试平台(默认加载中断测试镜像) |
| `pipelined-cpu/tools/rv_asm.py` | — | 迷你汇编器:`.asm` → 机器码 hex(`.lst` 带反汇编清单) |
| `pipelined-cpu/tools/hex2vh.py` | — | hex →`pipelined-cpu/srcs/board/cpu21_board_rom.vh`(`rom[i] = 32'h…;` 初始化语句) |
拆分只调整了模块边界与文件组织,端口、时序与行为完全等价:使用同一 ROM 程序
在 xsim 下对拆分前后逐周期对比,波形输出与最终统计(周期、停顿、气泡、分支
跳转、预测成败)完全一致。
## 上板(Nexys4DDR + Vivado)
处理器顶层 `cpu21_riscv_redirect_int_bpb` 有 775 位调试端口(各级 PC/IR、计数器、
BPB/中断观测值)。如果直接把它设为实现顶层,这些端口会全部变成引脚,导致
`[Place 30-415] IO Placement failed due to overutilization`(占用 775 个 IO,
而 `xc7a100tcsg324-1` 只有 210 个)。因此上板时用 `cpu21_board_top` 作为顶层,
只引出板上真正需要的 23 个引脚,其余调试端口不连接。
### 引脚分配(`cpu21_board_top.xdc`)
| 端口 | 引脚 | 板上元件 | 说明 |
| ------------- | ----------- | ------------------------- | ---------------------------------------------- |
| `clk100` | E3 | 100 MHz 晶振 | 经`CLK_DIV` 分频后给 CPU |
| `rst_n` | C12 | 按键 CPU_RESETN(低有效) | 异步置位、与 CPU 时钟同步释放 |
| `go` | N17 | 按键 BTNC | 对应原电路“继续运行”(`ecall` 停机后恢复执行) |
| `irq_sw[2:0]` | J15/L16/M13 | 拨码开关 SW0–SW2 | 三路中断源,拨动即触发(内部同步并锁存) |
| `led[15:0]` | H17…V11 | LED0–LED15 | MMIO(`LED_ADDR`)写入值的低 16 位 |
| `led_we` | R12 | LED16_B | MMIO 写入指示,每写一次拉长约 2.7 s |
### 时钟:为什么还保留 2 分频
乘除法原本是 ALU 里的组合运算(`a*b`、`a%b`),实测关键路径约 94 ns(325 级逻辑、
286 个 CARRY4),在 100 MHz 下必然不收敛,所以之前只能把时钟 16 分频到 6.25 MHz。
现在乘除法改成了 `pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v` 里的 **32 拍迭代多周期单元**,关键路径上
不再有组合乘/除,也不需要再分频了;但把所有加法器/比较器都换成迭代后,在 100 MHz
下仍有约 1.1 ns 的违例——那是 **EX 级“分支判决 / 中断保存 → BPB 更新”这条与乘除法
无关的组合路径**(逻辑只占约 3 ns,剩下 7~8 ns 全是布线)。
因此在 `cpu21_board_top` 里保留一个参数化的分频器 `CLK_DIV`,默认取 **2(50 MHz,
周期 20 ns)**,既比原来的 16 分频快 8 倍,又留出充足余量:
| 配置 | CPU 时钟 | 实测 WNS(xc7a100tcsg324-1) | 说明 |
| ------------ | -------- | ---------------------------- | ------------------------------------- |
| `CLK_DIV=1` | 100 MHz | -1.1 ns ✗ | 违例路径与乘除法无关(分支/中断/BPB) |
| `CLK_DIV=2` | 50 MHz | **+5.1 ns** ✓ | 默认,余量约 25% |
| `CLK_DIV=16` | 6.25 MHz | +6.1 ns ✓ | 改造前的设置,现在没必要了 |
分频后的时钟在综合/实现时由 Vivado 自动插入 BUFG 走全局时钟网络(RTL 里不例化
`BUFG` 原语,这样在不认识 Xilinx 原语的仿真器里也能直接仿真)。XDC 中必须用
`create_generated_clock` 声明这个分频时钟,否则时序分析会按 10 ns 去要求 CPU
内部路径而误报大量违例:
```tcl
create_generated_clock -name cpu_clk -source [get_ports { clk100 }] \
-divide_by 2 [get_pins { clk_div_reg/Q }]
```
改分频比时 RTL 的 `CLK_DIV` 与 XDC 的 `-divide_by` 要同时改。
### 片内 ROM / RAM
- ROM:`ROM_WORDS = 1024` 字(4 KB),组合读,取指地址 `instr_addr[11:2]`;
内容由 `` `include "cpu21_board_rom.vh" `` 给出(与 `cpu21_board_top.v` 同目录,
文件里是 1024 条
`rom[i] = 32'h…;`,放在 `initial` 里,等价于 `$readmemh`,且不依赖文件路径)。
- RAM:`RAM_WORDS = 1024` 字(4 KB),字节写使能 + 组合读,只响应
`data_addr < 4 KB`,因此 MMIO 地址(默认 `0x1000`)不会落进 RAM。
- 两个 `initial` 块都用 NOP(`32'h0000_0013`)或 0 预填充,ROM 未使用区域是 NOP,
这样即使程序跑飞也不会执行到随机指令而乱写寄存器。
### 演示程序的生成与仿真
```powershell
# 在工程目录 pipelined-cpu/ 下执行:
# 1) 汇编 + 生成 ROM 初始化文件
python tools/rv_asm.py programs/board_demo.asm _tmp/board_demo.hex
python tools/hex2vh.py _tmp/board_demo.hex srcs/board/cpu21_board_rom.vh 1024
# 2) 上板顶层的 RTL 仿真(默认程序延时约 20 ms,只需确认第一次写入 LED=0001)
xvlog srcs/rtl/*.v srcs/board/cpu21_board_top.v srcs/tb/tb_board_top.v
xelab -top tb_board_top -snapshot board_sim
xsim board_sim -tclbatch run_quit.tcl
```
`board_demo.asm` 是一段 16 步跑马灯(`0001 → 0002 → … → 8000 → 0001`),
每步之间用软件循环延时约 20 ms(50 MHz 下)。仓库里同时提供了两份 ROM 初始化
文件,由 `BOARD_ROM_FAST` 宏选择(综合流程不会定义该宏):
| 文件 | 用途 | 每步耗时 |
| -------------------------------------------------- | ------------------------------ | -------- |
| `pipelined-cpu/srcs/board/cpu21_board_rom.vh` | 综合/上板(默认) | 约 20 ms |
| `pipelined-cpu/srcs/board/cpu21_board_rom_fast.vh` | 仅仿真:一次跑完整个跑马灯序列 | 2.6 µs |
```powershell
# 快速版仿真:编译时加上 -d BOARD_ROM_FAST(xvlog 与 xelab 都要带)
xvlog -d BOARD_ROM_FAST srcs/rtl/*.v srcs/board/cpu21_board_top.v srcs/tb/tb_board_top.v
xelab -top tb_board_top -snapshot board_sim_fast -d BOARD_ROM_FAST
```
两份 ROM 初始化文件由 `programs/board_demo.asm`(默认,≈ 20 ms/步)与
`programs/board_demo_fast.asm`(快速版,≈ 2.6 µs/步,只差延时循环的初值)生成:
```powershell
python tools/rv_asm.py programs/board_demo_fast.asm _tmp/board_demo_fast.hex
python tools/hex2vh.py _tmp/board_demo_fast.hex srcs/board/cpu21_board_rom_fast.vh 1024
```
### Vivado 工程设置与实现结果
Vivado 工程 `pipelined-cpu/pipelined-cpu.xpr` 被 `.gitignore` 排除,需要手动设置:
1. `sources_1` 顶层设为 `cpu21_board_top`(纯 RTL 仿真时也可切回
`cpu21_riscv_redirect_int_bpb`);
2. 加入 `pipelined-cpu/srcs/board/cpu21_board_top.v`、`pipelined-cpu/srcs/constrs/cpu21_board_top.xdc`,
`pipelined-cpu/srcs/board/cpu21_board_rom.vh` 的文件类型设成 `Verilog Header`(不能当普通源码,
否则会被当成顶层候选);
3. 三个 testbench 放在**仿真 fileset**(`sim_1` 顶层 `tb_cpu21_riscv_redirect_int_bpb`、
`sim_2` 顶层 `tb_no_intr`),不参与综合,因此不会再出现“testbench 被当成顶层”
导致的 `[Place 30-494] The design is empty`。
`xc7a100tcsg324-1` 上的实现结果(乘除法改为多周期单元、`CLK_DIV=2` = 50 MHz):
| 项目 | 结果 |
| -------- | -------------------------------------------------------------------------------------------- |
| 综合 | `synth_design Complete!` |
| 实现 | `route_design Complete!`、`write_bitstream Complete!`,`WNS = +5.109 ns`(无 Failed Timing) |
| 资源 | LUT 3420 (5.39%)、FF 2507、**DSP 0**(组合乘法器已消失)、IOB 23 (10.95%) |
| bit 文件 | `pipelined-cpu/pipelined-cpu.runs/impl_1/cpu21_board_top.bit` |
对比改造前(组合乘除法 + `CLK_DIV=16`):LUT 5400 → 3420、DSP 12 → 0、
CPU 时钟 6.25 MHz → 50 MHz(快 8 倍),并且乘除法不再位于关键路径上。
上板操作:用 Vivado 打开工程 → `Open Hardware Manager` → `Program Device`,选上述
`.bit`。复位后跑马灯立即从 `0001` 开始逐位左移(演示程序不执行 `ecall`,所以不用
按 `go`;`go` 只对执行了 `ecall` 停机、等待继续运行的程序有意义),LED16_B 每次写入时点亮。
## 回归测试
工程里带了一个 xsim 回归脚本,在 `pipelined-cpu/` 目录下运行:
```powershell
powershell -ExecutionPolicy Bypass -File tools\run_regress.ps1
```
它会按顺序做四件事:
1. 编译 `srcs/rtl/` 下的处理器源码;
2. 跑 4 个指令测试(`muldiv_test`、`isa_test`、`lui_test`、`mmio_test`),把测试平台
打印的 LED 序列与 `programs/*.expected` 逐值比对;
3. 跑两个流水基线(`tb_no_intr`、`tb_cpu21_riscv_redirect_int_bpb`),打印 FINAL 统计行;
4. 跑板级顶层(`-d BOARD_ROM_FAST`),检查跑马灯 LED 序列。
最后汇总 PASS/FAIL,并以失败数作为退出码。编译与仿真的中间文件都写在
`%TEMP%\cpu21_regress` 下,不会污染工程目录;需要 `xvlog`/`xelab`/`xsim` 在 PATH 上。
期望结果(2026-09,多周期乘除法 + `CLK_DIV=2`):
| 测试 | 期望 |
| ---- | ---- |
| muldiv_test / isa_test / lui_test / mmio_test | 13 / 24 / 4 / 5 个 LED 值全部匹配 |
| tb_no_intr(benchmark,5560 周期) | `stalls=1528 bubbles=1830 cond_taken=639 uncond=71 pred_ok=687 pred_fail=80` |
| tb_cpu21_riscv_redirect_int_bpb(中断,6003 周期) | `stalls=963 bubbles=1205 cond_taken=547 uncond=28 pred_ok=557 pred_fail=87` |
| 板级(快速 ROM) | 跑马灯 `0001 → … → 8000` 循环,每步约 2.6 µs |
## 工程分享(Tcl 一键重建)
`pipelined-cpu/` 已整理成“自包含 + 可直接分享”的工程目录,设计文件都在里面,
别人拿到整个文件夹后**不需要改任何路径**:
```text
pipelined-cpu/
├─ create_project.tcl 一键重建脚本(用 [info script] 定位自身目录)
├─ srcs/rtl/*.v 处理器 RTL(13 个文件)
├─ srcs/board/*.v|*.vh 板级顶层、片内 ROM 初始化(含 __FAST 仿真版本)
├─ srcs/constrs/*.xdc 引脚约束 + 板级母版
├─ srcs/tb/*.v 三个测试平台
├─ srcs/rom/*.hex 测试平台加载的 ROM 镜像
├─ tools/、programs/ 迷你汇编器与测试程序(重生成 ROM 用)
├─ cpu21_riscv_redirect_int_bpb说明.md
└─ pipelined-cpu.xpr(+ *.runs/…) 本机工程,分享时可删
```
用法(在 Vivado Tcl Shell 里,或命令行 `vivado -mode batch -source create_project.tcl`):
```tcl
cd <pipelined-cpu 所在目录>
source create_project.tcl
```
脚本会就地创建 `pipelined-cpu.xpr`,并配好:
- `sources_1` 顶层 `cpu21_board_top`(处理器 RTL + 板级顶层 + 片内 ROM 初始化,
`cpu21_board_rom.vh` 标记为 `Verilog Header`);
- `constrs_1`:`cpu21_board_top.xdc`(引脚 + `CLK_DIV` 分频时钟)与板级母版;
- `sim_1`(顶层 `tb_cpu21_riscv_redirect_int_bpb`)与 `sim_2`(顶层 `tb_no_intr`);
测试平台会依次探测 ROM 镜像,其中 `../../../../../srcs/rom/` 对应 GUI 仿真的
工作目录(`<proj>.sim/sim_1/behav/xsim`),所以创建完就能直接 `Run Simulation`;
- 两个仿真 fileset 的 `xsim.simulate.runtime` 设为 `all`,避免 GUI 仿真只跑 1000 ns
就停下(看起来像卡死)。
分享前建议删掉生成目录:`*.xpr`、`*.runs`、`*.sim`、`*.cache`、`*.hw`、`*.gen`、
`*.ip_user_files`、`.Xil`。实测只带 `create_project.tcl` + `srcs/` + `pipelined-cpu/tools/` +
`programs/` + 说明文档约 **0.3 MB**,在任意路径重建工程并综合成功。
> 源文件只有一份,就在 `pipelined-cpu/srcs/`(`tools/`、`programs/` 同理),
> 直接在工程目录里改即可,没有需要同步的副本。
也可以用 Vivado 自动生成:
```tcl
write_project_tcl -force -paths_relative_to <工程目录> <工程目录>/create_project.tcl
```
注意自动生成的版本会把源路径写成**生成时的绝对路径**、并且要求 `origin_dir` 指向
工程目录,所以要手动改两行才能给别人用:
```tcl
set origin_dir [file dirname [file normalize [info script]]]
create_project ${_xil_proj_name_} $origin_dir -part xc7a100tcsg324-1
```
+135 -8
View File
@@ -1,6 +1,6 @@
# ============================================================================
# cpu21-riscv 流水线 CPU —— 一键重建 Vivado 工程
# FPGA:Nexys4DDR / xc7a100tcsg324-1(Vivado 2025.2 实测通过)
# FPGA:Nexys4DDR / xc7a100tcsg324-1(Vivado 2020.2 实测:建工程 + 综合均通过)
#
# 目录结构(本脚本所在目录就是工程目录):
# create_project.tcl 本脚本
@@ -13,11 +13,15 @@
# 说明文档:cpu21_riscv_redirect_int_bpb说明.md
#
# 用法:
# 1) 建议先删掉本目录下的 pipelined-cpu.xpr,以及 *.runs / *.sim / *.cache /
# *.hw / *.gen / *.ip_user_files 等生成目录(不删会在下面报错提示);
# 2) 在 Vivado Tcl Shell 里执行: source create_project.tcl
# 1) 在 Vivado Tcl Shell 里执行: source create_project.tcl
# 或命令行: vivado -mode batch -source create_project.tcl -log create.log
# 2) 脚本会**先自动清理旧工程**再新建:只删生成物(*.xpr / *.srcs / *.runs /
# *.sim / *.cache / *.hw / *.gen / *.ip_user_files / .Xil / xsim.dir /
# vivado*.jou|log|str / webtalk.*),srcs/、tools/、programs/ 一律不动。
# 不想自动清理(比如想在旧工程上追加设置)就把下面的 clean_first 改成 0。
# 3) 工程就地创建:pipelined-cpu.xpr(源文件仍在 srcs/ 下,不会被搬动)。
# 4) 若提示 .xpr 删不掉,说明有 Vivado(含 Hardware Manager)正开着那个工程,
# 关掉它再跑一遍即可。
#
# 脚本用 [info script] 定位自身目录,所以整个文件夹拷到任何路径都能直接用。
# 若想用 Vivado 自动生成的版本,见说明文档“工程分享”一节。
@@ -47,14 +51,74 @@ if {[llength $missing] > 0} {
return 1
}
if {[file exists "$origin_dir/$proj_name.xpr"]} {
puts "ERROR: 工程已存在:$origin_dir/$proj_name.xpr"
puts " 请先删除 .xpr(以及 *.runs / *.sim 等生成目录)后重跑本脚本。"
# ---------------------------------------------------------------- 清理旧工程
# 一键重建:先删掉旧工程和所有生成目录。工程完全由 srcs/ + 本脚本决定,
# 所以删掉不会丢任何源文件;不想自动清理就把 clean_first 改成 0。
set clean_first 1
if {$clean_first} {
# 必须删掉的工程目录:任何一个删不掉都不能继续(否则 Vivado 会拒绝建工程)
set proj_items [list \
"$origin_dir/$proj_name.xpr" \
"$origin_dir/$proj_name.srcs" \
"$origin_dir/$proj_name.runs" \
"$origin_dir/$proj_name.sim" \
"$origin_dir/$proj_name.cache" \
"$origin_dir/$proj_name.hw" \
"$origin_dir/$proj_name.gen" \
"$origin_dir/$proj_name.ip_user_files" \
"$origin_dir/$proj_name.lpr" \
"$origin_dir/.Xil"]
# 顺带清掉的杂项:删不掉也无所谓(本进程自己会占着 vivado.jou / .log / .str)
set junk_items [list \
"$origin_dir/xsim.dir" \
"$origin_dir/webtalk.jou" \
"$origin_dir/webtalk.log" \
"$origin_dir/usage_statistics_webtalk.html" \
"$origin_dir/usage_statistics_webtalk.xml"]
foreach pat {vivado*.jou vivado*.log vivado*.str} {
foreach f [glob -nocomplain "$origin_dir/$pat"] { lappend junk_items $f }
}
set blocked {}
set removed 0
foreach {group items} [list proj $proj_items junk $junk_items] {
foreach d $items {
if {![file exists $d]} { continue }
if {[catch {file delete -force $d} err]} {
puts " WARNING: 删不掉 [file tail $d]($err)"
if {$group eq "proj"} { lappend blocked [file tail $d] }
} else {
puts " 已删除:[file tail $d]"
incr removed
}
}
}
puts "旧工程清理完成(删除 $removed 项)"
if {[llength $blocked] > 0} {
puts "ERROR: 这些工程目录删不掉:$blocked"
puts " 它们正被另一个 Vivado(GUI / Hardware Manager)占用。"
puts " 请关闭 Vivado 后重跑本脚本。"
return 1
}
} else {
if {[file exists "$origin_dir/$proj_name.xpr"] || [file exists "$origin_dir/$proj_name.runs"]} {
puts "ERROR: 工程已存在且 clean_first = 0:$origin_dir/$proj_name.xpr"
puts " 请先删除它,或把 clean_first 改成 1 让脚本自动清理。"
return 1
}
}
# ---------------------------------------------------------------- 建工程
create_project $proj_name $origin_dir -part $part_name
# 只用 -part 建工程,**不依赖"板卡文件"**:Vivado 默认不带 Nexys4DDR 的 board 文件,
# 所以 GUI 新建向导的 Boards 页里找不到它;board 只影响"选板/板级预设",
# 不影响综合与实现(引脚全在 srcs/constrs/*.xdc 里显式约束)。
# 若以后装了 Digilent 板卡文件(data/boards/board_files/nexys4-ddr),想按板卡建:
# create_project $proj_name $origin_dir -part $part_name \
# -board_part digilentinc.com:nexys4ddr:part0:1.1
# (那样工程会记上板卡,别人没装板卡文件就打不开了,所以默认不这么写。)
# -force:万一还有残留目录(例如被占用的 .runs)也能覆盖,不至于卡死。
create_project $proj_name $origin_dir -part $part_name -force
# 设计源文件:处理器 RTL + 板级顶层 + 片内 ROM 初始化
add_files -norecurse -fileset sources_1 [glob "$origin_dir/srcs/rtl/*.v"]
@@ -64,6 +128,59 @@ add_files -norecurse -fileset sources_1 [list \
set_property file_type {Verilog Header} \
[get_files "$origin_dir/srcs/board/cpu21_board_rom.vh"]
# 默认跑哪个内置程序(同一时间只应启用一个):
# use_intrupt_sllsb = 1 -> 课程新版「多级中断测试 + 附加 SLL/SB/BLTU」
# (programs/risc-v-intrupt-sllsb.asm / .hex)
# use_intrupt_ccab = 1 -> 上一版中断测试(programs/risc-v-intrupt-ccab.*)
# use_saolei = 1 -> 扫雷(saolei/tools/build.ps1 生成)
# 三个都为 0 -> 走马灯 demo
set use_intrupt_sllsb 0
set use_intrupt_ccab 1
set use_saolei 0
# 中断测试程序的 ROM 初始化。
set intrupt_ccab_rom "$origin_dir/srcs/board/risc-v-intrupt-ccab.vh"
if {[file exists $intrupt_ccab_rom]} {
add_files -norecurse -fileset sources_1 [list $intrupt_ccab_rom]
set_property file_type {Verilog Header} [get_files $intrupt_ccab_rom]
} else {
puts "NOTE: 还没有 $intrupt_ccab_rom"
}
# 课程新版中断测试(附加 SLL/SB/BLTU)的 ROM 初始化。
set intrupt_sllsb_rom "$origin_dir/srcs/board/risc-v-intrupt-sllsb.vh"
if {[file exists $intrupt_sllsb_rom]} {
add_files -norecurse -fileset sources_1 [list $intrupt_sllsb_rom]
set_property file_type {Verilog Header} [get_files $intrupt_sllsb_rom]
} else {
puts "NOTE: 还没有 $intrupt_sllsb_rom(用 tools/hex2vh.py 生成)"
}
# 扫雷程序的 ROM 初始化(由 saolei/tools/build.ps1 生成,可能还不存在)。
# 综合/仿真时定义宏 SAOLEI_ROM 就会用它替换上面的跑马灯 ROM。
set saolei_rom "$origin_dir/srcs/board/cpu21_saolei_rom.vh"
if {[file exists $saolei_rom]} {
add_files -norecurse -fileset sources_1 [list $saolei_rom]
set_property file_type {Verilog Header} [get_files $saolei_rom]
} else {
puts "NOTE: 还没有 $saolei_rom —— 需要先跑 saolei/tools/build.ps1 生成"
}
# 关键:sources_1 里定义对应宏,cpu21_board_top.v 的 `ifdef 链才会选中那个 ROM
# (宏只影响综合用的 sources_1;仿真 fileset 仍各自用 tb 自带的 ROM)
if {$use_intrupt_sllsb && [file exists $intrupt_sllsb_rom]} {
set_property verilog_define {RISC_V_INTRUPT_SLLSB_ROM} [get_filesets sources_1]
puts "NOTE: sources_1 已定义 RISC_V_INTRUPT_SLLSB_ROM —— 上板跑「中断测试 + SLL/SB/BLTU」"
} elseif {$use_intrupt_ccab && [file exists $intrupt_ccab_rom]} {
set_property verilog_define {RISC_V_INTRUPT_CCAB_ROM} [get_filesets sources_1]
puts "NOTE: sources_1 已定义 RISC_V_INTRUPT_CCAB_ROM —— 上板跑 risc-v-intrupt-ccab"
} elseif {$use_saolei && [file exists $saolei_rom]} {
set_property verilog_define {SAOLEI_ROM} [get_filesets sources_1]
puts "NOTE: sources_1 已定义 SAOLEI_ROM —— 上板跑扫雷;想跑走马灯把本脚本里 use_saolei 改成 0"
} else {
puts "NOTE: 未定义 SAOLEI_ROM —— 上板跑走马灯 demo"
}
# 约束(cpu21_board_top.xdc 是引脚与分频时钟约束)
add_files -norecurse -fileset constrs_1 [glob "$origin_dir/srcs/constrs/*.xdc"]
@@ -99,6 +216,16 @@ foreach fs {sim_1 sim_2} {
# ---------------------------------------------------------------- 结果汇总
puts "工程已创建:$origin_dir/$proj_name.xpr"
puts " sources_1 顶层 = cpu21_board_top(上板:时钟分频 + 片内 ROM/RAM + 引脚)"
if {$use_intrupt_sllsb && [file exists $intrupt_sllsb_rom]} {
set selected_program {risc-v-intrupt-sllsb (RISC_V_INTRUPT_SLLSB_ROM) 中断+SLL/SB/BLTU}
} elseif {$use_intrupt_ccab && [file exists $intrupt_ccab_rom]} {
set selected_program {risc-v-intrupt-ccab (RISC_V_INTRUPT_CCAB_ROM)}
} elseif {$use_saolei && [file exists $saolei_rom]} {
set selected_program {扫雷 (SAOLEI_ROM)}
} else {
set selected_program {走马灯 demo}
}
puts " sources_1 程序 = $selected_program"
puts " sim_1 顶层 = tb_cpu21_riscv_redirect_int_bpb(中断/分支预测)"
puts " sim_2 顶层 = tb_no_intr(指令集测试,ROM 镜像在 srcs/rom/)"
puts " 下一步:Run Synthesis → Run Implementation → Generate Bitstream,或 Run Simulation"
+170
View File
@@ -0,0 +1,170 @@
# ============================================================================
# create_project_saolei.tcl —— 一键重建【扫雷专用】Vivado 工程
# FPGA:Nexys4DDR / xc7a100tcsg324-1(Vivado 2020.2)
#
# 与 create_project.tcl 的区别(那个是"通用/可切换程序"的脚本):
# * 只装配扫雷需要的东西:rtl/*.v + board/cpu21_board_top.v + 扫雷 ROM 头文件
# * sources_1 与 sim_1 都固定定义宏 **SAOLEI_ROM**(不用再改任何开关)
# * 工程名是独立的 pipelined-cpu-saolei,跟通用工程 pipelined-cpu 互不影响,
# 两个工程可以并存;本脚本只清理自己的生成物。
# * sim_1 顶层直接就是 tb_saolei(GUI 里 Run Simulation 就能出画面 dump)
#
# 用法(在 Vivado Tcl Shell 里):
# source create_project_saolei.tcl
# 或命令行:
# vivado -mode batch -source create_project_saolei.tcl -log create_saolei.log
#
# 前置条件:扫雷的 ROM 镜像必须先由 C 程序生成
# cd saolei
# powershell -ExecutionPolicy Bypass -File tools\build.ps1 `
# -Prefix 'C:\Users\29653\tools\riscv\xpack-riscv-none-elf-gcc-15.2.0-1\bin\riscv-none-elf-'
# → 产出 saolei/build/saolei.hex 与 pipelined-cpu/srcs/board/cpu21_saolei_rom.vh
# (改了 saolei/sw/*.c 之后要重跑这条命令,再综合/再跑仿真)
#
# 不想自动清理旧工程:把下面的 clean_first 改成 0。
# 想建完就顺手综合:把 run_synth 改成 1(2020.2 上约 1~2 分钟)。
#
# 附:没有 VGA 显示器时怎么"看/玩"扫雷
# 看:本工程的 sim_1 跑 tb_saolei,会把显存 dump 成文本;再用
# py saolei\tools\fb2png.py <dump.txt> <out.png> 还原成 640x480 画面。
# 玩:见 pipelined-cpu/tools/play_saolei.tcl(命令行交互式仿真,一步一截图)。
# ============================================================================
set origin_dir [file dirname [file normalize [info script]]]
set proj_name "pipelined-cpu-saolei"
set part_name "xc7a100tcsg324-1"
# ---------------------------------------------------------------- 自检
set saolei_rom "$origin_dir/srcs/board/cpu21_saolei_rom.vh"
set required [list \
"$origin_dir/srcs/board/cpu21_board_top.v" \
"$origin_dir/srcs/constrs/cpu21_board_top.xdc" \
"$origin_dir/srcs/tb/tb_saolei.v"]
if {[llength [glob -nocomplain "$origin_dir/srcs/rtl/*.v"]] == 0} {
lappend required "$origin_dir/srcs/rtl/*.v"
}
set missing {}
foreach f $required { if {![file exists $f]} { lappend missing $f } }
if {[llength $missing] > 0} {
puts "ERROR: 缺少下列文件,请确认 srcs/ 目录完整:"
foreach f $missing { puts " $f" }
return 1
}
if {![file exists $saolei_rom]} {
puts "ERROR: 还没有扫雷 ROM 镜像:$saolei_rom"
puts " 先在 saolei/ 目录跑 tools\\build.ps1 生成它(见本脚本头部注释)。"
return 1
}
# ---------------------------------------------------------------- 清理旧工程
# 和 create_project.tcl 同一套策略:只删生成物,srcs/ 等源文件一律不动。
set clean_first 1
set run_synth 0
set synth_jobs 4
if {$clean_first} {
set proj_items [list \
"$origin_dir/$proj_name.xpr" \
"$origin_dir/$proj_name.srcs" \
"$origin_dir/$proj_name.runs" \
"$origin_dir/$proj_name.sim" \
"$origin_dir/$proj_name.cache" \
"$origin_dir/$proj_name.hw" \
"$origin_dir/$proj_name.gen" \
"$origin_dir/$proj_name.ip_user_files" \
"$origin_dir/$proj_name.lpr" \
"$origin_dir/.Xil"]
set junk_items [list \
"$origin_dir/xsim.dir" \
"$origin_dir/webtalk.jou" \
"$origin_dir/webtalk.log" \
"$origin_dir/usage_statistics_webtalk.html" \
"$origin_dir/usage_statistics_webtalk.xml"]
foreach pat {vivado*.jou vivado*.log vivado*.str} {
foreach f [glob -nocomplain "$origin_dir/$pat"] { lappend junk_items $f }
}
set blocked {}
set removed 0
foreach {group items} [list proj $proj_items junk $junk_items] {
foreach d $items {
if {![file exists $d]} { continue }
if {[catch {file delete -force $d} err]} {
puts " WARNING: 删不掉 [file tail $d]($err)"
if {$group eq "proj"} { lappend blocked [file tail $d] }
} else {
puts " 已删除:[file tail $d]"
incr removed
}
}
}
puts "旧工程清理完成(删除 $removed 项,只动 $proj_name.* 与自己产生的临时文件)"
if {[llength $blocked] > 0} {
puts "ERROR: 这些工程目录删不掉:$blocked"
puts " 它们正被另一个 Vivado(GUI / Hardware Manager)占用,关掉后重跑。"
return 1
}
} else {
if {[file exists "$origin_dir/$proj_name.xpr"] || [file exists "$origin_dir/$proj_name.runs"]} {
puts "ERROR: 工程已存在且 clean_first = 0:$origin_dir/$proj_name.xpr"
puts " 请先删掉它,或把 clean_first 改成 1。"
return 1
}
}
# ---------------------------------------------------------------- 建工程
# 只用 -part,不依赖 Digilent 板卡文件(引脚全在 srcs/constrs/*.xdc 里)。
create_project $proj_name $origin_dir -part $part_name -force
# 设计源文件:处理器 RTL + 板级顶层 + 扫雷 ROM
add_files -norecurse -fileset sources_1 [glob "$origin_dir/srcs/rtl/*.v"]
add_files -norecurse -fileset sources_1 [list \
"$origin_dir/srcs/board/cpu21_board_top.v" \
$saolei_rom]
set_property file_type {Verilog Header} [get_files $saolei_rom]
# 关键:两个 fileset 都定义 SAOLEI_ROM,cpu21_board_top.v 的 `ifdef 链才会选中扫雷 ROM。
# (sources_1 管综合/上板,sim_1 管 GUI 仿真;sim_1 不定义的话会跑成走马灯。)
set_property verilog_define {SAOLEI_ROM} [get_filesets sources_1]
set_property verilog_define {SAOLEI_ROM} [get_filesets sim_1]
# 约束(引脚 + 时钟分频)
add_files -norecurse -fileset constrs_1 [glob "$origin_dir/srcs/constrs/*.xdc"]
# 仿真:扫雷调试台。POINTER_SAMPLE_DIV 由 tb 自己 defparam 压小,上板仍是 50000。
add_files -norecurse -fileset sim_1 [list "$origin_dir/srcs/tb/tb_saolei.v"]
# ---------------------------------------------------------------- 顶层与顺序
set_property top cpu21_board_top [get_filesets sources_1]
set_property top tb_saolei [get_filesets sim_1]
update_compile_order -fileset sources_1
update_compile_order -fileset sim_1
# GUI 仿真默认只跑 1000 ns,这里改成跑完(否则看起来像卡住)
catch { set_property -name {xsim.simulate.runtime} -value {all} -objects [get_filesets sim_1] }
# ---------------------------------------------------------------- 可选:顺手综合
if {$run_synth} {
puts "开始综合 synth_1(run_synth = 1)..."
reset_run synth_1
launch_runs synth_1 -jobs $synth_jobs
wait_on_run synth_1
if {[get_property PROGRESS [get_runs synth_1]] ne "100%"} {
puts "ERROR: 综合未完成,状态 = [get_property STATUS [get_runs synth_1]]"
return 1
}
puts "综合完成:0 error 即可 Run Implementation → Generate Bitstream。"
}
# ---------------------------------------------------------------- 结果汇总
puts ""
puts "工程已创建:$origin_dir/$proj_name.xpr"
puts " sources_1 顶层 = cpu21_board_top,程序 = 扫雷(已定义 SAOLEI_ROM)"
puts " sources_1 ROM = [file tail $saolei_rom]"
puts " sim_1 顶层 = tb_saolei(跑仿真会打印显存 dump,用 fb2png.py 还原成 PNG)"
puts " 约束 = [file tail [lindex [glob "$origin_dir/srcs/constrs/*.xdc"] 0]]"
puts " 下一步:Run Synthesis → Run Implementation → Generate Bitstream"
puts " (本工程已固定扫雷,不用再改 create_project.tcl 里的三个开关)"
puts " 上板操作:BTNU/D/L/R 移动光标,BTNC = 左键,SW3 = 插旗模式"
puts " 注意:CPU 时钟 = 100 MHz / CLK_DIV(默认 2) = 50 MHz;改分频比时"
puts " cpu21_board_top.v 的 CLK_DIV 与 cpu21_board_top.xdc 的 -divide_by 要一起改。"
@@ -0,0 +1,498 @@
# =============================================================================
# 注意:上板/仿真真正使用的镜像是 programs/risc-v-intrupt-ccab.hex(270 字),
# 本文件是课程原始清单。由 tools/adapt_intrupt_ccab.py 生成 ROM 时会做六处适配:
# 1) 三个 ISR 里的 "addi a7,zero,10 + ecall" 会让这颗 CPU 停机(a7 != 34 的
# ecall 是"停机等 go_i"的约定),中断服务程序跑到那里就卡住、后面的中断显示
# 循环永远不执行 -> ROM 里换成两条空操作;
# 2) 原来的 "addi a7,zero,34 + ecall" 显示改成写 MMIO 的 LED 寄存器(0x1000),
# 并且每个显示点都改为调用 DISP_DLY 加约 10 ms 延时 —— 否则 50 MHz 下刷新
# 太快,数码管的动态扫描根本分辨不出来,只会看到一片亮;
# 3) DISP_DLY 追加在程序末尾(0x438),因此所有指令地址、特别是三个中断入口
# 0x0AC / 0x1E4 / 0x308 都保持不变;
# 4) **hex 里 ISR2 / ISR3 的现场恢复(出栈)顺序被写乱了**(本清单是正确的,
# ISR1 的 hex 也是正确的)。ISR3 把 s1 从 s3 的槽位弹出,于是 s1 被写成
# s3 保存的 0xFFFFF111;ISR3 返回后,正在跑 BLT 测试的 ISR2 的循环计数 s1
# 就变成 -3823,循环要跑约 170 万周期才退出,表现就是"二级/三级中断返回
# 之后卡住";ISR2 的 s0/s2/a0/a7/s1/t3/t1 也全部错位。适配脚本按本清单的
# 正确 LIFO 顺序改回 12 条 lw 的目标寄存器(字数不变、地址不动)。
# 5) **ISR3 的 SB 测试用了它自己没有保存的寄存器**(t1 当地址、t3 当计数、
# s2 当增量)。ISR3 的现场只保存了 s1/a7/a0/s0/s3/s4/s5/s6/uepc,所以:
# 若 IRQ3 直接抢占正在跑 DIVU 测试的 ISR1(ISR1 用 t3 递减计数),ISR1 的
# 计数会被改掉;ISR1 恢复后 `addi t3,t3,-1` 得到负数,`bne t3,zero` 永远
# 成立,DIVU 循环再也回不来 —— 表现就是“拨了 SW0 再拨 SW2 之后卡住”;
# 主程序的跑马灯步长 t1 也会被改掉。适配脚本把这 14 处寄存器域改成 ISR3
# 自己保存的 s4/s5/s6(SB 测试结束后紧接着的 IntLoop3 会重新给它们赋值,
# 所以这样换是安全的),字数与地址不变。
# 6) **ISR 不能破坏被打断代码的 ra**:三个 ISR 都保存 s1/a7/a0/s0/s3/s4/s5/s6,
# 但都没有保存 ra,而它们要调用显示例程,"jal ra,DISP_DLY" 会把 ra 写成 ISR
# 内部的返回地址。如果中断恰好落在被打断代码依赖 ra 的位置(典型就是主程序
# 正停在 DISP_DLY 末尾的 jalr zero,0(ra) 那一条上),中断返回后就会跳进 ISR
# 代码里,一路执行到 ISR 结尾的 csrrci zero,0x4,1,把 MIE 永久清零:表现
# 就是“跑一阵子之后拨开关再也不响应,只能复位”。窗口只有 1~2 拍,平时很
# 难遇到(仿真里把延时改到极小才会碰上)。适配脚本给 ISR 另加一份显示例程
# DISP_DLY_ISR(0x458),链接寄存器改用 a0、显示值走 a7(这两个寄存器三个
# ISR 都保存/恢复),于是 ISR 完全不碰 ra;ISR 的显示点由 "jal ra,DISP_DLY +
# nop" 换成 "add a7,a0,zero + jal a0,DISP_DLY_ISR",仍是 2 字换 2 字,
# 字数与地址不变。
# =============================================================================
.text
addi sp, zero, 2000 #内存堆栈初始化
#############################################################
#走马灯测?,测试addi,andi,slli,srli,srai,or,ori,nor,ecall LED按走马灯方式来回显示数据
#############################################################
.text
START:
addi s0,zero,1
slli s3, s0, 31 # s3=0x80000000
srai s3, s3, 31 # s3=0xFFFFFFFF
add s0,zero,zero # s0=0
addi s2,zero,12
addi s6,zero,8 #走马灯计数
zmd_loop:
addi s0, s0, 1 #计算下一个走马灯的数
andi s0, s0, 15
#######################################
addi t0,zero,8
addi t1,zero,1
left:
slli s3, s3, 4 #走马灯左移
or s3, s3, s0
add a0,zero,s3 # display s3
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
sub t0,t0,t1
bne t0,zero,left
#######################################
addi s0, s0, 1 #计算下一个走马灯的数
addi t6,zero,15
and s0, s0, t6
slli s0, s0, 28
addi t0,zero,8
addi t1,zero,1
zmd_right:
srli s3, s3, 4 #走马灯右移
or s3, s3, s0
add a0,zero,s3 # display s3
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
sub t0,t0,t1
bne t0,zero,zmd_right
srli s0, s0, 28
#######################################
sub s6,s6,t1
beq s6,zero, exit
j zmd_loop
exit:
add t0,zero,zero
xori t0,t0,-1 #test nor ori
slli t0,t0,16
ori t0,t0,-1
add a0,zero,t0 # display t0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
j START # loop forever
#InteruptProgram1:
#############################################################################################
# exceptoin 1
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram1:
sw t0, 0(sp) #保护t0现场
addi sp, sp, 4
sw t3, 0(sp) #保护t3现场
addi sp, sp, 4
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# DIVU测试
addi t0,zero,2 # /2
addi s1,zero, 0x11
slli s1,s1,8
addi s1,s1,0x11
slli s1,s1,16
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi t3,zero,28 #ѭ������
divu_branch:
divu s1,s1,t0 #����ָ��
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi t3,t3, -1
bne t3,zero,divu_branch #ѭ��
nop # 原为 addi a7,zero,10 + ecall:这颗 CPU 会停机,改成空操作
################# 中断服务
addi s6,zero,1 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop1:
add s0,zero,s6
IntLeftShift1:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
bne s0, zero, IntLeftShift1
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop1
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
addi sp, sp, -4
lw t3, 0(sp) #恢复t3现场
addi sp, sp, -4
lw t0, 0(sp) #恢复t0现场
################ 中断返回
uret #同步开中断,uepc-->pc
#InteruptProgram2:
#############################################################################################
# exceptoin 2
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram2:
sw t1, 0(sp) #保护t1现场
addi sp, sp, 4
sw t3, 0(sp) #保护t3现场
addi sp, sp, 4
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw s2, 0(sp) #保护s2现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# BLT测试
addi s1,zero,-15 #初始值
blt_branch:
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi s1,s1,1
blt s1,zero,blt_branch #当前指令
nop # 原为 addi a7,zero,10 + ecall:这颗 CPU 会停机,改成空操作
################# 中断服务
addi s6,zero,2 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop2:
add s0,zero,s6
IntLeftShift2:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
bne s0, zero, IntLeftShift2
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop2
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp) #恢复s0现场
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s2, 0(sp) #恢复s2现场
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
addi sp, sp, -4
lw t3, 0(sp) #恢复t3现场
addi sp, sp, -4
lw t1, 0(sp) #恢复t1现场
################ 中断返回
uret #同步开中断,uepc-->pc
#InteruptProgram3:
#############################################################################################
# exceptoin 3
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram3:
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# SB测试
# 注:ROM 镜像里下面这段的 t1/t3/s2 已改成 ISR3 自己保存的 s4/s5/s6
# (t1 当地址 -> s4,t3 当计数 -> s5,s2 当增量 -> s6),避免抢占 ISR1
# 时破坏 ISR1 的 DIVU 循环计数,详见文件头部第 5 项。
addi t1,zero,0 #init_addr
addi t3,zero,32 #counter
#sb写入 01,02,03,04
addi s1,zero, 0x00 #
addi s2,zero, 0x01 #
sb_store:
sb s1,(t1)
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
add s1,s1,s2 #data +1
addi t1,t1,1 # addr ++
addi t3,t3,-1 #counter
bne t3,zero,sb_store
addi t3,zero,8
addi t1,zero,0 # addr
sb_branch:
lw s1,(t1) #读出数据
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi t1,t1,4
addi t3,t3, -1
bne t3,zero,sb_branch
nop # 原为 addi a7,zero,10 + ecall:这颗 CPU 会停机,改成空操作
################# 中断服务
addi s6,zero,3 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop3:
add s0,zero,s6
IntLeftShift3:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
bne s0, zero, IntLeftShift3
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop3
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
################ 中断返回
uret #同步开中断,uepc-->pc
# -----------------------------------------------------------------------------
# DISP_DLY —— 显示 + 延时(对应 ROM 末尾 0x438 起的 8 条指令;11 个显示点都用
# "jal ra, DISP_DLY" 调用它)
#
# sw ra, 256(sp) # 保存返回地址(故意存在 sp+256,见下面的说明)
# lui a7, 1 # a7 = 0x1000 = LED_ADDR
# sw a0, 0(a7) # 把 a0 写进 LED 寄存器(数码管显示它的 8 位十六进制)
# lui t2, 0xFFFC0 # 计数初值为负(-262144),向上数到 0 ≈ 10 ms @50 MHz
# 1:addi t2, t2, 1
# blt t2, zero, 1b
# lw ra, 256(sp)
# jalr zero, ra, 0
#
# 为什么返回地址存在 sp+256,而不是按常规压在 sp-8 的栈帧里:
# 本程序的 ISR 用的是"先把 sp 加上去、再往里存"的向上增长栈,一层最多 48 字节,
# 最多嵌套 4 层(IRQ_STACK_DEPTH=4)共 192 字节 —— 也就是说 [sp, sp+192) 是中断
# 随时可能写到的区域。而显示调用既可能被中断打断,也可能从 ISR 内部发出,所以:
# * 若压在 sp-8(常规做法),从 ISR 里调用时会正好盖掉 ISR 存好的 s6 / uepc;
# uepc 一坏,uret 就返回到 ISR 内部地址,最后执行到 ISR 尾部的 csrrci 把 MIE
# 永久清零,之后再也不响应中断(实测就是这个现象);
# * 放在 sp+256 越过了中断可能压栈的 192 字节区域,嵌套中断打断延时也不会破坏
# 它;每次调用各用自己的 sp+256,嵌套调用互不影响。
#
# 延时计数用"负数向上数到 0"而不是"递减到 0":万一 t2 被嵌套调用改掉,循环只会
# 在 t2 >= 0 时立即退出,不会像递减写法那样回绕成 40 亿次而死等。
# -----------------------------------------------------------------------------
DISP_DLY:
sw ra, 256(sp)
lui a7, 1
sw a0, 0(a7)
lui t2, 0xFFFC0
DISP_DLY_WAIT:
addi t2, t2, 1
blt t2, zero, DISP_DLY_WAIT
lw ra, 256(sp)
jalr zero, ra, 0
@@ -12,8 +12,8 @@
00499993
0089e9b3
01300533
02200893
00000073
000018B7
00A8A023
406282b3
fe0294e3
00140413
@@ -25,8 +25,8 @@ fe0294e3
0049d993
0089e9b3
01300533
02200893
00000073
000018B7
00A8A023
406282b3
fe0294e3
01c45413
@@ -38,8 +38,8 @@ fff2c293
01029293
fff2e293
00500533
02200893
00000073
000018B7
00A8A023
f5dff06f
00512023
00410113
@@ -71,13 +71,13 @@ f5dff06f
01148493
01049493
00900533
02200893
00000073
000018B7
00A8A023
01c00e13
0254d4b3
00900533
02200893
00000073
000018B7
00A8A023
fffe0e13
fe0e16e3
00a00893
@@ -89,8 +89,8 @@ fe0e16e3
00441413
014469b3
01300533
02200893
00000073
000018B7
00A8A023
fe0416e3
415a0a33
fe0a10e3
@@ -147,8 +147,8 @@ ffc10113
0040e073
ff100493
00900533
02200893
00000073
000018B7
00A8A023
00148493
fe04c8e3
00a00893
@@ -160,8 +160,8 @@ fe04c8e3
00441413
014469b3
01300533
02200893
00000073
000018B7
00A8A023
fe0416e3
415a0a33
fe0a10e3
@@ -218,8 +218,8 @@ ffc10113
00100913
00930023
00900533
02200893
00000073
000018B7
00A8A023
012484b3
00130313
fffe0e13
@@ -228,8 +228,8 @@ fe0e12e3
00000313
00032483
00900533
02200893
00000073
000018B7
00A8A023
00430313
fffe0e13
fe0e14e3
@@ -242,8 +242,8 @@ fe0e14e3
00441413
014469b3
01300533
02200893
00000073
000018B7
00A8A023
fe0416e3
415a0a33
fe0a10e3
@@ -0,0 +1,477 @@
.text
addi sp, zero, 2000 #�ڴ��ջ��ʼ��
#############################################################
#�����Ʋ�?,����addi,andi,slli,srli,srai,or,ori,nor,ecall LED�������Ʒ�ʽ������ʾ����
#############################################################
.text
START:
addi s0,zero,1
slli s3, s0, 31 # s3=0x80000000
srai s3, s3, 31 # s3=0xFFFFFFFF
add s0,zero,zero # s0=0
addi s2,zero,12
addi s6,zero,8 #�����Ƽ���
zmd_loop:
addi s0, s0, 1 #������һ�������Ƶ���
andi s0, s0, 15
#######################################
addi t0,zero,8
addi t1,zero,1
left:
slli s3, s3, 4 #����������
or s3, s3, s0
add a0,zero,s3 # display s3
addi a7,zero,34 # system call for LED display
ecall # display
sub t0,t0,t1
bne t0,zero,left
#######################################
addi s0, s0, 1 #������һ�������Ƶ���
addi t6,zero,15
and s0, s0, t6
slli s0, s0, 28
addi t0,zero,8
addi t1,zero,1
zmd_right:
srli s3, s3, 4 #����������
or s3, s3, s0
add a0,zero,s3 # display s3
addi a7,zero,34 # system call for LED display
ecall # display
sub t0,t0,t1
bne t0,zero,zmd_right
srli s0, s0, 28
#######################################
sub s6,s6,t1
beq s6,zero, exit
j zmd_loop
exit:
add t0,zero,zero
xori t0,t0,-1 #test nor ori
slli t0,t0,16
ori t0,t0,-1
add a0,zero,t0 # display t0
addi a7,zero,34 # system call for LED display
ecall # display
j START # loop forever
#InteruptProgram1:
#############################################################################################
# exceptoin 1
# ʹ��?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# �����ֳ�
InteruptProgram1:sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepcѹջ����
addi sp, sp, 4
################# ���ж�
sw t0,0(sp)
addi sp,sp,4
sw t1,0(sp)
addi sp,sp,4
sw t2,0(sp)
addi sp,sp,4
sw t3,0(sp)
addi sp,sp,4
sw t4,0(sp)
addi sp,sp,4
sw t5,0(sp)
addi sp,sp,4
sw t6,0(sp)
addi sp,sp,4
sw s1,0(sp)
addi sp,sp,4
sw s2,0(sp)
addi sp,sp,4
csrrsi zero,0x4,1 #ustatus.MIE=1
################# �жϷ���
addi s6,zero,1 #�ж�?1,2,3 ��ͬ�жϺ���ʾֵ��һ��
addi s4,zero,3 #ѭ��������ʼֵ
addi s5,zero,1 #�������ۼ�ֵ
IntLoop1:
add s0,zero,s6
IntLeftShift1:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift1
sub s4,s4,s5 #ѭ�������ݼ�
bne s4, zero, IntLoop1
################# ���ж�
csrrci zero,0x4,1 #ustatus.MIE=0
############### ����ָ�� ����(SLL/SB/BLTU ֮һ) ###############
addi t0,zero,1
addi t1,zero,3
addi s1,zero,0x8
slli s1,s1,8
addi s1,s1,0x76
add a0,zero,s1
addi a7,zero,34
ecall
addi t3,zero,8
sll_branch:
sll s1,s1,t0
sll s1,s1,t1
add a0,zero,s1
addi a7,zero,34
ecall
addi t3,t3,-1
bne t3,zero,sll_branch
addi sp,sp,-4
lw s2,0(sp)
addi sp,sp,-4
lw s1,0(sp)
addi sp,sp,-4
lw t6,0(sp)
addi sp,sp,-4
lw t5,0(sp)
addi sp,sp,-4
lw t4,0(sp)
addi sp,sp,-4
lw t3,0(sp)
addi sp,sp,-4
lw t2,0(sp)
addi sp,sp,-4
lw t1,0(sp)
addi sp,sp,-4
lw t0,0(sp)
################# �ָ��ֳ�
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
################ �жϷ���
uret #ͬ�����жϣ�uepc-->pc
#InteruptProgram2:
#############################################################################################
# exceptoin 2
# ʹ��?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# �����ֳ�
InteruptProgram2:sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepcѹջ����
addi sp, sp, 4
################# ���ж�
sw t0,0(sp)
addi sp,sp,4
sw t1,0(sp)
addi sp,sp,4
sw t2,0(sp)
addi sp,sp,4
sw t3,0(sp)
addi sp,sp,4
sw t4,0(sp)
addi sp,sp,4
sw t5,0(sp)
addi sp,sp,4
sw t6,0(sp)
addi sp,sp,4
sw s1,0(sp)
addi sp,sp,4
sw s2,0(sp)
addi sp,sp,4
csrrsi zero,0x4,1 #ustatus.MIE=1
################# �жϷ���
addi s6,zero,2 #�ж�?1,2,3 ��ͬ�жϺ���ʾֵ��һ��
addi s4,zero,3 #ѭ��������ʼֵ
addi s5,zero,1 #�������ۼ�ֵ
IntLoop2:
add s0,zero,s6
IntLeftShift2:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift2
sub s4,s4,s5 #ѭ�������ݼ�
bne s4, zero, IntLoop2
################# ���ж�
csrrci zero,0x4,1 #ustatus.MIE=0
############### ����ָ�� ����(SLL/SB/BLTU ֮һ) ###############
addi t1,zero,0
addi t3,zero,32
addi s1,zero,0x00
addi s2,zero,0x01
sb_store:
sb s1,0(t1)
add a0,zero,s1
addi a7,zero,34
ecall
add s1,s1,s2
addi t1,t1,1
addi t3,t3,-1
bne t3,zero,sb_store
addi t3,zero,8
addi t1,zero,0
sb_branch:
lw s1,0(t1)
add a0,zero,s1
addi a7,zero,34
ecall
addi t1,t1,4
addi t3,t3,-1
bne t3,zero,sb_branch
addi sp,sp,-4
lw s2,0(sp)
addi sp,sp,-4
lw s1,0(sp)
addi sp,sp,-4
lw t6,0(sp)
addi sp,sp,-4
lw t5,0(sp)
addi sp,sp,-4
lw t4,0(sp)
addi sp,sp,-4
lw t3,0(sp)
addi sp,sp,-4
lw t2,0(sp)
addi sp,sp,-4
lw t1,0(sp)
addi sp,sp,-4
lw t0,0(sp)
################# �ָ��ֳ�
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
################ �жϷ���
uret #ͬ�����жϣ�uepc-->pc
#InteruptProgram3:
#############################################################################################
# exceptoin 3
# ʹ��?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# �����ֳ�
InteruptProgram3:sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepcѹջ����
addi sp, sp, 4
################# ���ж�
sw t0,0(sp)
addi sp,sp,4
sw t1,0(sp)
addi sp,sp,4
sw t2,0(sp)
addi sp,sp,4
sw t3,0(sp)
addi sp,sp,4
sw t4,0(sp)
addi sp,sp,4
sw t5,0(sp)
addi sp,sp,4
sw t6,0(sp)
addi sp,sp,4
sw s1,0(sp)
addi sp,sp,4
sw s2,0(sp)
addi sp,sp,4
csrrsi zero,0x4,1 #ustatus.MIE=1
################# �жϷ���
addi s6,zero,3 #�ж�?1,2,3 ��ͬ�жϺ���ʾֵ��һ��
addi s4,zero,3 #ѭ��������ʼֵ
addi s5,zero,1 #�������ۼ�ֵ
IntLoop3:
add s0,zero,s6
IntLeftShift3:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift3
sub s4,s4,s5 #ѭ�������ݼ�
bne s4, zero, IntLoop3
################# ���ж�
csrrci zero,0x4,1 #ustatus.MIE=0
############### ����ָ�� ����(SLL/SB/BLTU ֮һ) ###############
addi s1,zero,-15
bltu_branch:
add a0,zero,s1
addi a7,zero,34
ecall
addi s1,s1,1
bltu zero,s1,bltu_branch
add a0,zero,s1
addi a7,zero,34
ecall
addi sp,sp,-4
lw s2,0(sp)
addi sp,sp,-4
lw s1,0(sp)
addi sp,sp,-4
lw t6,0(sp)
addi sp,sp,-4
lw t5,0(sp)
addi sp,sp,-4
lw t4,0(sp)
addi sp,sp,-4
lw t3,0(sp)
addi sp,sp,-4
lw t2,0(sp)
addi sp,sp,-4
lw t1,0(sp)
addi sp,sp,-4
lw t0,0(sp)
################# �ָ��ֳ�
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
################ �жϷ���
uret #ͬ�����жϣ�uepc-->pc
@@ -0,0 +1,344 @@
7d000113
00100413
01f41993
41f9d993
00000433
00c00913
00800b13
00140413
00f47413
00800293
00100313
00499993
0089e9b3
01300533
02200893
00000073
406282b3
fe0294e3
00140413
00f00f93
01f47433
01c41413
00800293
00100313
0049d993
0089e9b3
01300533
02200893
00000073
406282b3
fe0294e3
01c45413
406b0b33
000b0463
f95ff06f
000002b3
fff2c293
01029293
fff2e293
00500533
02200893
00000073
f5dff06f
01112023
00410113
00a12023
00410113
00812023
00410113
01312023
00410113
01412023
00410113
01512023
00410113
01612023
00410113
04106b73
01612023
00410113
00512023
00410113
00612023
00410113
00712023
00410113
01c12023
00410113
01d12023
00410113
01e12023
00410113
01f12023
00410113
00912023
00410113
01212023
00410113
0040e073
00100b13
00300a13
00100a93
01600433
00441413
014469b3
01300533
02200893
00000073
fe0416e3
415a0a33
fe0a10e3
0040f073
00100293
00300313
00800493
00849493
07648493
00900533
02200893
00000073
00800e13
005494b3
006494b3
00900533
02200893
00000073
fffe0e13
fe0e14e3
ffc10113
00012903
ffc10113
00012483
ffc10113
00012f83
ffc10113
00012f03
ffc10113
00012e83
ffc10113
00012e03
ffc10113
00012383
ffc10113
00012303
ffc10113
00012283
ffc10113
00012b03
041b1073
ffc10113
00012b03
ffc10113
00012a83
ffc10113
00012a03
ffc10113
00012983
ffc10113
00012403
ffc10113
00012503
ffc10113
00012883
00200073
01112023
00410113
00a12023
00410113
00812023
00410113
01312023
00410113
01412023
00410113
01512023
00410113
01612023
00410113
04106b73
01612023
00410113
00512023
00410113
00612023
00410113
00712023
00410113
01c12023
00410113
01d12023
00410113
01e12023
00410113
01f12023
00410113
00912023
00410113
01212023
00410113
0040e073
00200b13
00300a13
00100a93
01600433
00441413
014469b3
01300533
02200893
00000073
fe0416e3
415a0a33
fe0a10e3
0040f073
00000313
02000e13
00000493
00100913
00930023
00900533
02200893
00000073
012484b3
00130313
fffe0e13
fe0e12e3
00800e13
00000313
00032483
00900533
02200893
00000073
00430313
fffe0e13
fe0e14e3
ffc10113
00012903
ffc10113
00012483
ffc10113
00012f83
ffc10113
00012f03
ffc10113
00012e83
ffc10113
00012e03
ffc10113
00012383
ffc10113
00012303
ffc10113
00012283
ffc10113
00012b03
041b1073
ffc10113
00012b03
ffc10113
00012a83
ffc10113
00012a03
ffc10113
00012983
ffc10113
00012403
ffc10113
00012503
ffc10113
00012883
00200073
01112023
00410113
00a12023
00410113
00812023
00410113
01312023
00410113
01412023
00410113
01512023
00410113
01612023
00410113
04106b73
01612023
00410113
00512023
00410113
00612023
00410113
00712023
00410113
01c12023
00410113
01d12023
00410113
01e12023
00410113
01f12023
00410113
00912023
00410113
01212023
00410113
0040e073
00300b13
00300a13
00100a93
01600433
00441413
014469b3
01300533
02200893
00000073
fe0416e3
415a0a33
fe0a10e3
0040f073
ff100493
00900533
02200893
00000073
00148493
fe9068e3
00900533
02200893
00000073
ffc10113
00012903
ffc10113
00012483
ffc10113
00012f83
ffc10113
00012f03
ffc10113
00012e83
ffc10113
00012e03
ffc10113
00012383
ffc10113
00012303
ffc10113
00012283
ffc10113
00012b03
041b1073
ffc10113
00012b03
ffc10113
00012a83
ffc10113
00012a03
ffc10113
00012983
ffc10113
00012403
ffc10113
00012503
ffc10113
00012883
00200073
@@ -1,421 +0,0 @@
.text
addi sp, zero, 2000 #内存堆栈初始化
#############################################################
#走马灯测?,测试addi,andi,slli,srli,srai,or,ori,nor,ecall LED按走马灯方式来回显示数据
#############################################################
.text
START:
addi s0,zero,1
slli s3, s0, 31 # s3=0x80000000
srai s3, s3, 31 # s3=0xFFFFFFFF
add s0,zero,zero # s0=0
addi s2,zero,12
addi s6,zero,8 #走马灯计数
zmd_loop:
addi s0, s0, 1 #计算下一个走马灯的数
andi s0, s0, 15
#######################################
addi t0,zero,8
addi t1,zero,1
left:
slli s3, s3, 4 #走马灯左移
or s3, s3, s0
add a0,zero,s3 # display s3
addi a7,zero,34 # system call for LED display
ecall # display
sub t0,t0,t1
bne t0,zero,left
#######################################
addi s0, s0, 1 #计算下一个走马灯的数
addi t6,zero,15
and s0, s0, t6
slli s0, s0, 28
addi t0,zero,8
addi t1,zero,1
zmd_right:
srli s3, s3, 4 #走马灯右移
or s3, s3, s0
add a0,zero,s3 # display s3
addi a7,zero,34 # system call for LED display
ecall # display
sub t0,t0,t1
bne t0,zero,zmd_right
srli s0, s0, 28
#######################################
sub s6,s6,t1
beq s6,zero, exit
j zmd_loop
exit:
add t0,zero,zero
xori t0,t0,-1 #test nor ori
slli t0,t0,16
ori t0,t0,-1
add a0,zero,t0 # display t0
addi a7,zero,34 # system call for LED display
ecall # display
j START # loop forever
#InteruptProgram1:
#############################################################################################
# exceptoin 1
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram1:
sw t0, 0(sp) #保护t0现场
addi sp, sp, 4
sw t3, 0(sp) #保护t3现场
addi sp, sp, 4
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# DIVU测试
addi t0,zero,2 # /2
addi s1,zero, 0x11
slli s1,s1,8
addi s1,s1,0x11
slli s1,s1,16
add a0,zero,s1
addi a7,zero,34
ecall
addi t3,zero,28 #ѭ������
divu_branch:
divu s1,s1,t0 #����ָ��
add a0,zero,s1
addi a7,zero,34
ecall #�����ǰֵ
addi t3,t3, -1
bne t3,zero,divu_branch #ѭ��
addi a7,zero,10
################# 中断服务
addi s6,zero,1 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop1:
add s0,zero,s6
IntLeftShift1:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift1
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop1
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
addi sp, sp, -4
lw t3, 0(sp) #恢复t3现场
addi sp, sp, -4
lw t0, 0(sp) #恢复t0现场
################ 中断返回
uret #同步开中断,uepc-->pc
#InteruptProgram2:
#############################################################################################
# exceptoin 2
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram2:
sw t1, 0(sp) #保护t1现场
addi sp, sp, 4
sw t3, 0(sp) #保护t3现场
addi sp, sp, 4
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw s2, 0(sp) #保护s2现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# BLT测试
addi s1,zero,-15 #初始值
blt_branch:
add a0,zero,s1
addi a7,zero,34
ecall #输出当前值
addi s1,s1,1
blt s1,zero,blt_branch #当前指令
addi a7,zero,10
################# 中断服务
addi s6,zero,2 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop2:
add s0,zero,s6
IntLeftShift2:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift2
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop2
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp) #恢复s0现场
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s2, 0(sp) #恢复s2现场
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
addi sp, sp, -4
lw t3, 0(sp) #恢复t3现场
addi sp, sp, -4
lw t1, 0(sp) #恢复t1现场
################ 中断返回
uret #同步开中断,uepc-->pc
#InteruptProgram3:
#############################################################################################
# exceptoin 3
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram3:
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# SB测试
addi t1,zero,0 #init_addr
addi t3,zero,32 #counter
#sb写入 01,02,03,04
addi s1,zero, 0x00 #
addi s2,zero, 0x01 #
sb_store:
sb s1,(t1)
add a0,zero,s1
addi a7,zero,34 # system call for print
ecall # print
add s1,s1,s2 #data +1
addi t1,t1,1 # addr ++
addi t3,t3,-1 #counter
bne t3,zero,sb_store
addi t3,zero,8
addi t1,zero,0 # addr
sb_branch:
lw s1,(t1) #读出数据
add a0,zero,s1
addi a7,zero,34 # system call for print
ecall # print
addi t1,t1,4
addi t3,t3, -1
bne t3,zero,sb_branch
addi a7,zero,10 # system call for exit
################# 中断服务
addi s6,zero,3 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop3:
add s0,zero,s6
IntLeftShift3:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift3
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop3
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
################ 中断返回
uret #同步开中断,uepc-->pc
+386 -20
View File
@@ -9,12 +9,20 @@
// ([Place 30-415] IO Placement failed due to overutilization)。本模块只把
// 开发板真正需要的外设引出来,其余调试端口不连接,因此不占用引脚:
//
// clk100 (E3) -> CPU 时钟(100 MHz,经 CLK_DIV 分频后给 CPU)
// 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 路中断源
// led[15:0] -> 板载 LED[15:0]:显示 MMIO(LED_ADDR)写入值的低 16 位
// 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"` 给出
@@ -26,7 +34,7 @@
// -----------------------------------------------------------------------------
module cpu21_board_top #(
parameter [31:0] LED_ADDR = 32'h0000_1000,
parameter ROM_WORDS = 1024,
parameter ROM_WORDS = 8192,
parameter RAM_WORDS = 1024,
parameter [31:0] USTATUS_INIT = 32'h0000_0001,
// 板载 100 MHz 的分频比(1 = 直用 100 MHz,2 = 50 MHz,…)。
@@ -37,28 +45,98 @@ module cpu21_board_top #(
// 7~8 ns 都是布线);
// CLK_DIV=2( 50 MHz):WNS ≈ +8 ns,余量充足。
// 因此默认取 2,既比原来的 16 分频快 8 倍,又能保证时序收敛。
parameter integer CLK_DIV = 2
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]
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 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 / CLK_DIV(默认 2 = 50 MHz)给 CPU。
// 时钟: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 声明,
// 改分频比时两处要一起改。
// 分频后的时钟由 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;
@@ -67,7 +145,7 @@ module cpu21_board_top #(
if (arst) begin
clk_cnt <= 16'd0;
clk_div <= 1'b0;
end else if (clk_cnt == (CLK_DIV / 2 - 1)) begin
end else if (clk_cnt == (CPU_DIV / 2 - 1)) begin
clk_cnt <= 16'd0;
clk_div <= ~clk_div;
end else begin
@@ -95,23 +173,59 @@ module cpu21_board_top #(
wire [31:0] data_wdata;
wire [ 3:0] data_wstrb;
wire data_we;
wire [31:0] led_data;
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:组合读
// 指令 ROM:组合读(未用到的地址填 NOP)
// ---------------------------------------------------------------------------
reg [31:0] rom [0:ROM_WORDS-1];
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 BOARD_ROM_FAST
`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
// ---------------------------------------------------------------------------
@@ -145,13 +259,30 @@ module cpu21_board_top #(
// ---------------------------------------------------------------------------
cpu21_riscv_redirect_int_bpb #(
.LED_ADDR (LED_ADDR),
.USTATUS_INIT(USTATUS_INIT)
.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_sw),
.irq_i (irq_vec),
.go_i (go),
.instr_addr_o(instr_addr),
@@ -161,23 +292,258 @@ module cpu21_board_top #(
.data_we_o (data_we),
.led_data_o (led_data),
.led_valid_o(led_valid)
.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:显示 MMIO 写入值的低 16 位;写入指示拉长便于观察
// 板载 LED:三路中断指示;十六进制数据改由七段数码管显示。
//
// 第 k 路中断从"请求出现"开始点亮,一直亮到它的 ISR 执行完 uret(中断返回)
// 之后才熄灭:
// 置位:irq_vec[k] 的上升沿。中断控制器内部也是按上升沿响应中断的
// (cpu21_riscv_irq_ctrl 里的 irq_event),两者行为一致;开关一直
// 接通不会重复点亮,只有新的上升沿才会重新置位。
// 清零:服务级别 irq_level 从 k+1 **降下来**,说明第 k+1 级的中断已经返回。
// 注意必须判断“下降”:级别从 1 升到 2 是深层中断抢占,此时第 1 级
// 的中断还在服务中,LED 不能熄。中断可以嵌套,返回时按级别逐层降
// 下来,因此每一路都在自己的 ISR 结束时熄灭。
// ---------------------------------------------------------------------------
assign led = led_data[15:0];
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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+63 -2
View File
@@ -13,6 +13,11 @@ create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports { c
create_generated_clock -name cpu_clk -source [get_ports { clk100 }] \
-divide_by 2 [get_pins { clk_div_reg/Q }]
## 这里固定按最坏情况 50 MHz(-divide_by 2)约束。跑中断综合测试程序时 RTL 里的
## CPU_DIV 会变成 8(12.5 MHz,见 cpu21_board_top.v 的 CLK_DIV_INTRUPT),实际
## 时钟比这里的约束更慢,因此仍然满足——约束偏严,不会掩盖时序违例。
## (真要改分频比时,两处都要改;把这里的 -divide_by 调大反而会放松约束。)
## 复位按键(CPU_RESETN,低有效)与"继续运行"按键(BTNC)
set_property -dict { PACKAGE_PIN C12 IOSTANDARD LVCMOS33 } [get_ports { rst_n }]
set_property -dict { PACKAGE_PIN N17 IOSTANDARD LVCMOS33 } [get_ports { go }]
@@ -22,7 +27,8 @@ set_property -dict { PACKAGE_PIN J15 IOSTANDARD LVCMOS33 } [get_ports { irq_sw
set_property -dict { PACKAGE_PIN L16 IOSTANDARD LVCMOS33 } [get_ports { irq_sw[1] }]
set_property -dict { PACKAGE_PIN M13 IOSTANDARD LVCMOS33 } [get_ports { irq_sw[2] }]
## 16 位 LED:显示 MMIO 写入值的低 16 位
## 16 位 LED:LED[2:0] = 三路中断指示(请求出现即亮,ISR 执行 uret 返回后灭),
## LED[15:3] 熄灭
set_property -dict { PACKAGE_PIN H17 IOSTANDARD LVCMOS33 } [get_ports { led[0] }]
set_property -dict { PACKAGE_PIN K15 IOSTANDARD LVCMOS33 } [get_ports { led[1] }]
set_property -dict { PACKAGE_PIN J13 IOSTANDARD LVCMOS33 } [get_ports { led[2] }]
@@ -40,8 +46,63 @@ set_property -dict { PACKAGE_PIN V14 IOSTANDARD LVCMOS33 } [get_ports { led[13
set_property -dict { PACKAGE_PIN V12 IOSTANDARD LVCMOS33 } [get_ports { led[14] }]
set_property -dict { PACKAGE_PIN V11 IOSTANDARD LVCMOS33 } [get_ports { led[15] }]
## LED16_B:MMIO 写入指示(拉长约 0.34 s @50 MHz)
## LED16_B:MMIO 写入指示(约 0.34 s @50 MHz;中断程序的 12.5 MHz 下约 1.36 s)
set_property -dict { PACKAGE_PIN R12 IOSTANDARD LVCMOS33 } [get_ports { led_we }]
## 上板调试时可把 MMIO 写入的 LED 寄存器也引到 LED16_R/G,
## 但默认只用一个引脚,避免与 led_we 争用。
## ======================= 扫雷扩展外设 =======================
## 5 向按键:BTNC 已作 go(同时也当鼠标左键),另外 4 个当光标方向
set_property -dict { PACKAGE_PIN M18 IOSTANDARD LVCMOS33 } [get_ports { btnu }]
set_property -dict { PACKAGE_PIN P18 IOSTANDARD LVCMOS33 } [get_ports { btnd }]
set_property -dict { PACKAGE_PIN P17 IOSTANDARD LVCMOS33 } [get_ports { btnl }]
set_property -dict { PACKAGE_PIN M17 IOSTANDARD LVCMOS33 } [get_ports { btnr }]
## SW[3]:插旗模式(1 = 点击时插旗/取消旗)
set_property -dict { PACKAGE_PIN R15 IOSTANDARD LVCMOS33 } [get_ports { sw_flag }]
## SW[15]:选 PS/2 设备(0 = 键盘,1 = 鼠标);工程里插的是哪个就拨到哪边
set_property -dict { PACKAGE_PIN V10 IOSTANDARD LVCMOS33 } [get_ports { hid_sel }]
## 七段数码管(共阳极:位选/段选都是低有效)
set_property -dict { PACKAGE_PIN T10 IOSTANDARD LVCMOS33 } [get_ports { seg[0] }]
set_property -dict { PACKAGE_PIN R10 IOSTANDARD LVCMOS33 } [get_ports { seg[1] }]
set_property -dict { PACKAGE_PIN K16 IOSTANDARD LVCMOS33 } [get_ports { seg[2] }]
set_property -dict { PACKAGE_PIN K13 IOSTANDARD LVCMOS33 } [get_ports { seg[3] }]
set_property -dict { PACKAGE_PIN P15 IOSTANDARD LVCMOS33 } [get_ports { seg[4] }]
set_property -dict { PACKAGE_PIN T11 IOSTANDARD LVCMOS33 } [get_ports { seg[5] }]
set_property -dict { PACKAGE_PIN L18 IOSTANDARD LVCMOS33 } [get_ports { seg[6] }]
set_property -dict { PACKAGE_PIN H15 IOSTANDARD LVCMOS33 } [get_ports { dp }]
set_property -dict { PACKAGE_PIN J17 IOSTANDARD LVCMOS33 } [get_ports { an[0] }]
set_property -dict { PACKAGE_PIN J18 IOSTANDARD LVCMOS33 } [get_ports { an[1] }]
set_property -dict { PACKAGE_PIN T9 IOSTANDARD LVCMOS33 } [get_ports { an[2] }]
set_property -dict { PACKAGE_PIN J14 IOSTANDARD LVCMOS33 } [get_ports { an[3] }]
set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } [get_ports { an[4] }]
set_property -dict { PACKAGE_PIN T14 IOSTANDARD LVCMOS33 } [get_ports { an[5] }]
set_property -dict { PACKAGE_PIN K2 IOSTANDARD LVCMOS33 } [get_ports { an[6] }]
set_property -dict { PACKAGE_PIN U13 IOSTANDARD LVCMOS33 } [get_ports { an[7] }]
## VGA(640x480@60,25 MHz 像素节拍由 CPU 时钟使能产生)
set_property -dict { PACKAGE_PIN A3 IOSTANDARD LVCMOS33 } [get_ports { vga_r[0] }]
set_property -dict { PACKAGE_PIN B4 IOSTANDARD LVCMOS33 } [get_ports { vga_r[1] }]
set_property -dict { PACKAGE_PIN C5 IOSTANDARD LVCMOS33 } [get_ports { vga_r[2] }]
set_property -dict { PACKAGE_PIN A4 IOSTANDARD LVCMOS33 } [get_ports { vga_r[3] }]
set_property -dict { PACKAGE_PIN C6 IOSTANDARD LVCMOS33 } [get_ports { vga_g[0] }]
set_property -dict { PACKAGE_PIN A5 IOSTANDARD LVCMOS33 } [get_ports { vga_g[1] }]
set_property -dict { PACKAGE_PIN B6 IOSTANDARD LVCMOS33 } [get_ports { vga_g[2] }]
set_property -dict { PACKAGE_PIN A6 IOSTANDARD LVCMOS33 } [get_ports { vga_g[3] }]
set_property -dict { PACKAGE_PIN B7 IOSTANDARD LVCMOS33 } [get_ports { vga_b[0] }]
set_property -dict { PACKAGE_PIN C7 IOSTANDARD LVCMOS33 } [get_ports { vga_b[1] }]
set_property -dict { PACKAGE_PIN D7 IOSTANDARD LVCMOS33 } [get_ports { vga_b[2] }]
set_property -dict { PACKAGE_PIN D8 IOSTANDARD LVCMOS33 } [get_ports { vga_b[3] }]
set_property -dict { PACKAGE_PIN B11 IOSTANDARD LVCMOS33 } [get_ports { vga_hs }]
set_property -dict { PACKAGE_PIN B12 IOSTANDARD LVCMOS33 } [get_ports { vga_vs }]
## PS/2 鼠标:接**板载 USB HID 口**(板上芯片把 USB 键鼠转成 PS/2)
## Nexys4DDR_Master.xdc 里写明:ps2_clk = F4、ps2_data = B2
## 顶层参数 USE_PS2_MOUSE=1 启用;板载按键仍然可用(两路事件在顶层合并)
## 若改用 Pmod JA 上的 Pmod PS2,把下面两行的引脚换成 C17 / D18 即可
set_property -dict { PACKAGE_PIN F4 IOSTANDARD LVCMOS33 PULLUP true } [get_ports { ps2_clk }]
set_property -dict { PACKAGE_PIN B2 IOSTANDARD LVCMOS33 PULLUP true } [get_ports { ps2_data }]
+241 -22
View File
@@ -4,18 +4,46 @@
// -----------------------------------------------------------------------------
// MMIO:把外设寄存器映射到数据存储器地址空间。
//
// 目前实现 LED 数据寄存器(LED_ADDR 默认 0x0000_1000,位于 4 KB 数据 RAM 之外):
// * 写:sw / sb 命中该地址(按字对齐,即同一 32 位寄存器内的任意字节偏移
// 都算命中)时更新 LED 寄存器,led_valid_o 拉高一拍;sb 依据 wstrb_i 只
// 修改对应的字节通道(与 RAM 的字节写语义一致)。
// * 读:lw 命中该地址时 rdata_hit_o 拉高,顶层据此选择本模块的 rdata_o
// 而不是外部 data_rdata_i,从而支持把 LED 值读回。
// 地址映射(字节地址,都是"字对齐"译码:同一 32 位寄存器内的任意字节偏移都命中,
// 具体改哪几个字节由 wstrb_i 决定,与原来 LED 的语义一致):
//
// 以后要增加外设(拨码开关、UART 等)时只需在这里扩展地址译码,
// 处理器核心与流水线时序不需要改动。
// 0x0000_1000 LED 数据寄存器(**保持原有语义不变**,led_valid_o 拉高一拍)
// 0x0000_1004 七段数码管:8 位十六进制数(每 4 位一个数码管,低 4 位 = 最右)
// 0x0000_1100 + 4*k 鼠标寄存器组(见下)
// 0x0000_1140 + 4*k 显示控制器寄存器(见下)
// 0x0000_2000 - 0x2FFF 显存窗口:40x30 格、每格 2 字节(共 2400 字节)
//
// 鼠标寄存器(基址 0x1100):
// +0x00 STATUS RO [0] FIFO 非空 [5:1] 项数 [6] 中断使能 [7] FIFO 满
// +0x04 DATA RO [7:0] dx [15:8] dy [20:16] 按键(读无副作用)
// +0x08 ACK WO 写任意值 -> 弹出 FIFO 一项
// +0x0C CTRL RW [0] 中断使能(复位默认 1)
// +0x10 INPUT RO [15:0] 拨码开关 [20:16] 按键(原样)
// +0x14 TIME RO 自由运行计数器(随机种子)
// +0x18 FRAME RO VGA 帧计数
// +0x1C EVCNT RO 累计鼠标事件数
// +0x20 LEVEL RW [0] 写 1 -> 拉起 level_irq_o(软件中断,用作"关卡入口");
// 关卡程序返回前要写 0 清掉,否则下次没有上升沿
// +0x24 MAGIC RO 0xCAFE0002(自检用:读得到说明 MMIO 活着)
//
// 显示控制器寄存器(基址 0x1140):
// +0x00 CTRL RW [0] 光标可见 [6:4] 背景色(3 位调色板)
// +0x04 CURSOR RW [5:0] 列(0..39) [13:8] 行(0..29)
// +0x08 MAGIC RO 0xCAFE0001
//
// 显存本体不在本模块里:写操作通过 fb_* 端口直接送到板级顶层的双口 RAM(VGA 读
// 同一块 RAM),读操作把 fb_rdata_i 组合回读数据,保持"组合读"的时序不变。
//
// 两个中断源:
// mouse_irq_o = 中断使能 && 事件 FIFO 非空(顶层接到 irq_i[1] = IRQ2 = 入口 0x200)
// level_irq_o = LEVEL 寄存器(软件中断,顶层接到 irq_i[0] = IRQ1 = 入口 0x100 = 关卡)
//
// 以后要增加外设(UART、SD 卡等)时只需在这里扩展地址译码。
// -----------------------------------------------------------------------------
module cpu21_mmio #(
parameter [31:0] LED_ADDR = 32'h0000_1000
parameter [31:0] LED_ADDR = 32'h0000_1000,
parameter integer FB_WORDS = 600, // 600 字 = 2400 字节(40x30 格,每格 2 字节)
parameter integer FIFO_DEPTH = 16
) (
input wire clk,
input wire reset,
@@ -29,34 +57,225 @@ module cpu21_mmio #(
// MMIO 读回
output wire [31:0] rdata_o,
output wire rdata_hit_o,
// LED 外设
// LED 外设(原样保留)
output reg [31:0] led_data_o,
output reg led_valid_o
output reg led_valid_o,
// 七段数码管:8 位十六进制数
output reg [31:0] seg_data_o,
output reg seg_valid_o,
// 显存写端口(板级顶层的双口 RAM)
output wire fb_we_o,
output wire [ 9:0] fb_waddr_o,
output wire [31:0] fb_wdata_o,
output wire [ 3:0] fb_wstrb_o,
input wire [31:0] fb_rdata_i,
// 显示控制器寄存器(给 VGA 用)
output wire [31:0] disp_ctrl_o,
output wire [31:0] disp_cursor_o,
// 鼠标事件源(来自板级顶层:按键去抖 / PS/2 解析)
input wire mouse_ev_valid_i,
input wire [ 7:0] mouse_dx_i,
input wire [ 7:0] mouse_dy_i,
input wire [ 4:0] mouse_btn_i,
input wire [15:0] sw_i,
input wire frame_tick_i,
// 中断
output wire mouse_irq_o,
output wire level_irq_o
);
// 地址译码按“字”对齐:同一 32 位寄存器内的任意字节偏移都算命中,
// 具体修改哪几个字节由 wstrb_i 决定(sw 全字、sb 单字节)。
wire led_wr = mem_valid_i && mem_write_i && (addr_i[31:2] == LED_ADDR[31:2]);
wire led_rd = mem_valid_i && mem_read_i && (addr_i[31:2] == LED_ADDR[31:2]);
// ---------------------------------------------------------------------------
// 地址译码(都按"字"对齐:同一 32 位寄存器内的任意字节偏移都算命中)
// ---------------------------------------------------------------------------
localparam [31:0] SEG_ADDR = 32'h0000_1004;
localparam [31:0] MOUSE_BASE = 32'h0000_1100;
localparam [31:0] DISP_BASE = 32'h0000_1140;
localparam integer FIFO_AW = 4;
localparam [FIFO_AW:0] FIFO_FULL_V = FIFO_DEPTH[FIFO_AW:0];
assign rdata_hit_o = led_rd;
assign rdata_o = led_data_o;
wire sel_led = (addr_i[31:2] == LED_ADDR[31:2]);
wire sel_seg = (addr_i[31:2] == SEG_ADDR[31:2]);
wire sel_mouse = (addr_i[31:8] == MOUSE_BASE[31:8]) && (addr_i[7:6] == 2'b00);
wire sel_disp = (addr_i[31:8] == DISP_BASE[31:8]) && (addr_i[7:6] == 2'b01);
// 显存窗口 0x2000 - 0x2FFF:共 600 个字(每格两个字节)。
wire sel_fb = (addr_i[31:12] == 20'h00002);
wire [3:0] midx = addr_i[5:2];
wire [3:0] didx = addr_i[5:2];
wire [9:0] fb_widx = addr_i[11:2];
wire fb_ok = (fb_widx < FB_WORDS);
wire led_wr = mem_valid_i && mem_write_i && sel_led;
wire led_rd = mem_valid_i && mem_read_i && sel_led;
wire seg_wr = mem_valid_i && mem_write_i && sel_seg;
wire seg_rd = mem_valid_i && mem_read_i && sel_seg;
wire mo_wr = mem_valid_i && mem_write_i && sel_mouse;
wire mo_rd = mem_valid_i && mem_read_i && sel_mouse;
wire dp_wr = mem_valid_i && mem_write_i && sel_disp;
wire dp_rd = mem_valid_i && mem_read_i && sel_disp;
wire fb_wr = mem_valid_i && mem_write_i && sel_fb && fb_ok;
wire fb_rd = mem_valid_i && mem_read_i && sel_fb && fb_ok;
assign fb_we_o = fb_wr;
assign fb_waddr_o = fb_widx;
assign fb_wdata_o = wdata_i;
assign fb_wstrb_o = wstrb_i;
assign rdata_hit_o = led_rd | seg_rd | mo_rd | dp_rd | fb_rd;
// 字节写合并:sw 改 4 字节、sb 只改 1 字节(与 RAM 的字节写语义一致)
function [31:0] merge_bytes;
input [31:0] new_v;
input [31:0] old_v;
input [3:0] strb;
begin
merge_bytes[7:0] = strb[0] ? new_v[7:0] : old_v[7:0];
merge_bytes[15:8] = strb[1] ? new_v[15:8] : old_v[15:8];
merge_bytes[23:16] = strb[2] ? new_v[23:16] : old_v[23:16];
merge_bytes[31:24] = strb[3] ? new_v[31:24] : old_v[31:24];
end
endfunction
// ---------------------------------------------------------------------------
// LED:保持原有语义(led_valid_o 只拉高一拍)
// ---------------------------------------------------------------------------
always @(posedge clk or posedge reset) begin
if (reset) begin
led_data_o <= 32'b0;
led_valid_o <= 1'b0;
end else begin
// led_valid_o 只拉高一拍,作为“本周期有 LED 写入”的指示。
led_valid_o <= 1'b0;
if (led_wr) begin
if (wstrb_i[0]) led_data_o[7:0] <= wdata_i[7:0];
if (wstrb_i[1]) led_data_o[15:8] <= wdata_i[15:8];
if (wstrb_i[2]) led_data_o[23:16] <= wdata_i[23:16];
if (wstrb_i[3]) led_data_o[31:24] <= wdata_i[31:24];
led_data_o <= merge_bytes(wdata_i, led_data_o, wstrb_i);
led_valid_o <= 1'b1;
end
end
end
// ---------------------------------------------------------------------------
// 七段数码管:8 位十六进制数(低 4 位 = 最右边那个数码管)
// ---------------------------------------------------------------------------
always @(posedge clk or posedge reset) begin
if (reset) begin
seg_data_o <= 32'b0;
seg_valid_o <= 1'b0;
end else begin
seg_valid_o <= 1'b0;
if (seg_wr) begin
seg_data_o <= merge_bytes(wdata_i, seg_data_o, wstrb_i);
seg_valid_o <= 1'b1;
end
end
end
// ---------------------------------------------------------------------------
// 显示控制器寄存器(显存本体在板级顶层)
// ---------------------------------------------------------------------------
reg [31:0] disp_ctrl_q, disp_cursor_q;
assign disp_ctrl_o = disp_ctrl_q;
assign disp_cursor_o = disp_cursor_q;
// ---------------------------------------------------------------------------
// 鼠标:事件 FIFO + 寄存器 + 中断
// ---------------------------------------------------------------------------
reg [31:0] fifo[0:FIFO_DEPTH-1];
reg [FIFO_AW-1:0] wr_ptr_q, rd_ptr_q;
reg [FIFO_AW:0] cnt_q;
reg [31:0] mctrl_q, time_q, frame_q, evcnt_q;
reg level_q;
wire fifo_empty = (cnt_q == {FIFO_AW + 1{1'b0}});
wire fifo_full = (cnt_q == FIFO_FULL_V);
wire [31:0] fifo_head = fifo[rd_ptr_q];
wire irq_en = mctrl_q[0];
assign mouse_irq_o = irq_en && !fifo_empty;
assign level_irq_o = level_q;
wire push = mouse_ev_valid_i && !fifo_full;
wire pop = mo_wr && (midx == 4'd2) && !fifo_empty;
always @(posedge clk or posedge reset) begin
if (reset) begin
mctrl_q <= 32'h0000_0001; // 鼠标中断默认打开
time_q <= 32'd0;
frame_q <= 32'd0;
evcnt_q <= 32'd0;
level_q <= 1'b0;
wr_ptr_q <= {FIFO_AW{1'b0}};
rd_ptr_q <= {FIFO_AW{1'b0}};
cnt_q <= {(FIFO_AW + 1) {1'b0}};
disp_ctrl_q <= 32'h0000_0001; // 光标可见,背景色 0
disp_cursor_q <= 32'h0000_0000;
end else begin
time_q <= time_q + 32'd1;
if (frame_tick_i) frame_q <= frame_q + 32'd1;
if (push) begin
fifo[wr_ptr_q] <= {11'b0, mouse_btn_i, mouse_dy_i, mouse_dx_i};
wr_ptr_q <= wr_ptr_q + 1'b1;
evcnt_q <= evcnt_q + 32'd1;
end
if (pop) rd_ptr_q <= rd_ptr_q + 1'b1;
cnt_q <= cnt_q + (push ? 1'b1 : 1'b0) - (pop ? 1'b1 : 1'b0);
// 鼠标寄存器:CTRL(3) / LEVEL(8)
if (mo_wr) begin
case (midx)
4'd3: mctrl_q <= merge_bytes(wdata_i, mctrl_q, wstrb_i);
4'd8: level_q <= wdata_i[0];
default: ;
endcase
end
// 显示控制器:CTRL(0) / CURSOR(1)
if (dp_wr) begin
case (didx)
4'd0: disp_ctrl_q <= merge_bytes(wdata_i, disp_ctrl_q, wstrb_i);
4'd1: disp_cursor_q <= merge_bytes(wdata_i, disp_cursor_q, wstrb_i);
default: ;
endcase
end
end
end
// ---------------------------------------------------------------------------
// 读数据(组合,与"指令存储器组合读/数据存储器组合读"的时序一致)
// ---------------------------------------------------------------------------
reg [31:0] rdata_mux;
always @* begin
rdata_mux = 32'b0;
if (led_rd) begin
rdata_mux = led_data_o;
end else if (seg_rd) begin
rdata_mux = seg_data_o;
end else if (fb_rd) begin
rdata_mux = fb_rdata_i;
end else if (mo_rd) begin
case (midx)
4'd0: rdata_mux = {24'b0, fifo_full, irq_en, cnt_q[4:0], !fifo_empty};
4'd1: rdata_mux = fifo_head;
4'd3: rdata_mux = mctrl_q;
4'd4: rdata_mux = {11'b0, mouse_btn_i, sw_i};
4'd5: rdata_mux = time_q;
4'd6: rdata_mux = frame_q;
4'd7: rdata_mux = evcnt_q;
4'd8: rdata_mux = {31'b0, level_q};
4'd9: rdata_mux = 32'hCAFE_0002;
default: rdata_mux = 32'b0;
endcase
end else if (dp_rd) begin
case (didx)
4'd0: rdata_mux = disp_ctrl_q;
4'd1: rdata_mux = disp_cursor_q;
4'd2: rdata_mux = 32'hCAFE_0001;
default: rdata_mux = 32'b0;
endcase
end
end
assign rdata_o = rdata_mux;
endmodule
`default_nettype wire
+109
View File
@@ -0,0 +1,109 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// cpu21_pointer —— 把板上的 5 向按键变成"鼠标"事件源(板级顶层用它给 MMIO 送事件)。
//
// 事件单位:1/8 格。按键一次 = 8 个单位 = 光标移动一格,这样软件端用
// 纯整数加减(不需要任何除法)就能把累积量换算成格,配合 PS/2 鼠标时
// 手感也一致(PS/2 的原始计数直接按 1/8 格处理)。
//
// BTNU -> dy = -8 BTND -> dy = +8
// BTNL -> dx = -8 BTNR -> dx = +8
// BTNC -> 左键按下(btn[0] = 1,dx = dy = 0)
//
// 去抖方式:用 ~1 ms 的采样节拍(50 MHz 下计数 50000)采样两级同步后的按键,
// 与上一次的稳定值比较,只在"按下"的边沿产生事件。一次采样可能同时检测到
// 多个按下,此时用 press_pend_q 排队,由优先级选择器逐个上报(每个时钟周期
// 最多一个事件),因此不会丢按键。
// -----------------------------------------------------------------------------
module cpu21_pointer #(
// 采样节拍 = clk / (SAMPLE_DIV),默认 50 MHz / 50000 = 1 kHz
parameter integer SAMPLE_DIV = 50000
) (
input wire clk,
input wire reset,
input wire [4:0] btn_i, // {BTNC, BTNU, BTND, BTNL, BTNR}
output wire ev_valid_o, // 单周期脉冲
output wire [7:0] ev_dx_o, // 有符号,1/8 格
output wire [7:0] ev_dy_o,
output wire [4:0] ev_btn_o
);
// ---------------------------------------------------------------------------
// 1 ms 采样节拍
// ---------------------------------------------------------------------------
reg [15:0] div_q;
wire sample = (div_q == (SAMPLE_DIV - 1));
always @(posedge clk or posedge reset) begin
if (reset)
div_q <= 16'd0;
else if (sample)
div_q <= 16'd0;
else
div_q <= div_q + 16'd1;
end
// ---------------------------------------------------------------------------
// 两级同步 + 采样保持(采样率远低于按键抖动频率,等效去抖)
// ---------------------------------------------------------------------------
reg [4:0] btn_sync1_q, btn_sync2_q, btn_stable_q;
always @(posedge clk or posedge reset) begin
if (reset) begin
btn_sync1_q <= 5'b0;
btn_sync2_q <= 5'b0;
btn_stable_q <= 5'b0;
end
else begin
btn_sync1_q <= btn_i;
btn_sync2_q <= btn_sync1_q;
if (sample)
btn_stable_q <= btn_sync2_q;
end
end
// 只在按下的边沿产生新事件
wire [4:0] press_new = sample ? (btn_sync2_q & ~btn_stable_q) : 5'b0;
// ---------------------------------------------------------------------------
// 待上报队列 + 优先级选择(每周期最多上报一个)
// ---------------------------------------------------------------------------
reg [4:0] press_pend_q;
wire [4:0] pend_next = press_pend_q | press_new;
reg [4:0] sel;
always @* begin
sel = 5'b0;
// 注意:sel 的每一位必须和下面的输出一致,也必须和 pend_next 一一对应!
// sel[4]=点击 sel[3]=上(-dy) sel[2]=下(+dy) sel[1]=左(-dx) sel[0]=右(+dx)
// 而且 press_pend_q <= pend_next & ~sel 要求 sel 的位数与 pend_next 同域,
// 否则被选中的那一位清不掉、ev_valid_o 会永久为 1(中断线一直挂着)。
if (pend_next[4])
sel = 5'b10000; // BTNC:点击
else if (pend_next[3])
sel = 5'b01000; // BTNU:上
else if (pend_next[2])
sel = 5'b00100; // BTND:下
else if (pend_next[1])
sel = 5'b00010; // BTNL:左
else if (pend_next[0])
sel = 5'b00001; // BTNR:右
end
always @(posedge clk or posedge reset) begin
if (reset)
press_pend_q <= 5'b0;
else
press_pend_q <= pend_next & ~sel;
end
assign ev_valid_o = |pend_next;
assign ev_dx_o = sel[1] ? 8'd248 : (sel[0] ? 8'd8 : 8'd0); // 248 = -8 的补码
assign ev_dy_o = sel[3] ? 8'd248 : (sel[2] ? 8'd8 : 8'd0);
assign ev_btn_o = {4'b0, sel[4]};
endmodule
`default_nettype wire
+151
View File
@@ -0,0 +1,151 @@
`timescale 1ns / 1ps
// =============================================================================
// cpu21_ps2_kbd —— PS/2 键盘接收(扫描码集 2),把按键映射成"鼠标事件"
//
// 为什么映射成鼠标事件:saolei.c / isr.c 早就是按鼠标事件 {btn, dy, dx} 写的,
// 键盘只要产出**同样格式**的事件,软件一行都不用改。
//
// ↑ ↓ ← → 光标移动(dx/dy = ±8,恰好是软件里的"一格")
// W A S D 同上(别名,与 tools/play_saolei.tcl 一致)
// Enter / 空格 左键(开格)
// 左 Shift / F / C 右键(软件里右键 = 插旗)
//
// 方向约定跟软件对齐:**dy = +8 是向下**(与 cpu21_pointer 的 BTND、cpu21_ps2_mouse 一致),
// 所以 ↑ 发 dy = 248(= -8 的补码)。
//
// PS/2 键盘发扫描码(集 2):
// 0xF0 前缀 -> 下一个码是"松开"(break);0xE0 前缀 -> 下一个码是扩展键
// 方向键是扩展键:↑ = E0 75、↓ = E0 72、← = E0 6B、→ = E0 74
// 所以只对"按下"(make)产生事件。按住不动时键盘自己会发重复码,
// 于是按住方向键正好变成"连续移动"。
//
// 与 cpu21_ps2_mouse 共用同一根 PS/2 线:两个接收器都挂在线上,只有插着的那个
// 设备会产生有效数据,所以顶层把两者的事件一起 OR 起来即可,不用二选一。
// =============================================================================
module cpu21_ps2_kbd (
input wire clk,
input wire reset,
input wire ps2_clk_i,
input wire ps2_data_i,
output reg ev_valid_o, // 单周期脉冲
output reg [7:0] ev_dx_o, // 有符号,1/8 格(±8 = 一格)
output reg [7:0] ev_dy_o,
output reg [4:0] ev_btn_o, // bit0 = 左键, bit1 = 右键
output reg [15:0] rx_count_o // 收到的字节数(调试观察用)
);
// ---- 两级同步 ----
reg ps2_clk_s1, ps2_clk_s2, ps2_data_s1, ps2_data_s2;
always @(posedge clk 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;
end
else begin
ps2_clk_s1 <= ps2_clk_i;
ps2_clk_s2 <= ps2_clk_s1;
ps2_data_s1 <= ps2_data_i;
ps2_data_s2 <= ps2_data_s1;
end
end
wire clk_fall = ~ps2_clk_s1 & ps2_clk_s2;
reg [3:0] bit_cnt_q;
reg [10:0] frame_q;
reg brk_q; // 刚收到 0xF0:下一个码是松开
reg ext_q; // 刚收到 0xE0:下一个码是扩展键
// 11 位里 data + parity 共 9 位,PS/2 是**奇校验**:1 的个数应为奇数
function automatic parity_ok(input [8:0] dp); // dp = {parity, d7..d0}
parity_ok = ^dp;
endfunction
// 处理一个扫描码(按下才产生事件)
task handle_code;
input [7:0] sc;
reg pressed;
begin
rx_count_o <= rx_count_o + 16'd1;
pressed = (brk_q == 1'b0); // 0xF0 之后的码是"松开"
brk_q <= 1'b0;
if (sc == 8'hE0)
ext_q <= 1'b1; // 扩展前缀
else if (sc == 8'hF0)
brk_q <= 1'b1; // 断码前缀
else if (pressed) begin
if (ext_q) begin
case (sc)
8'h75: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd248; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // ↑
8'h72: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd8; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // ↓
8'h6B: begin ev_dx_o <= 8'd248; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // ←
8'h74: begin ev_dx_o <= 8'd8; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // →
default: ;
endcase
end
else begin
case (sc)
8'h5A: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00001; ev_valid_o <= 1'b1; end // Enter
8'h29: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00001; ev_valid_o <= 1'b1; end // 空格
8'h12: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00010; ev_valid_o <= 1'b1; end // 左 Shift
8'h2B: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00010; ev_valid_o <= 1'b1; end // F
8'h2E: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00010; ev_valid_o <= 1'b1; end // C
// W/A/S/D 也当方向键(和 tools/play_saolei.tcl 里的别名保持一致)
8'h1D: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd248; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // W = ↑
8'h1B: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd8; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // S = ↓
8'h1C: begin ev_dx_o <= 8'd248; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // A = ←
8'h23: begin ev_dx_o <= 8'd8; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // D = →
default: ;
endcase
end
end
// 一次前缀组合处理完,清掉扩展标志(松开码 E0 F0 xx 也走这里)
if ((sc != 8'hE0) && (sc != 8'hF0))
ext_q <= 1'b0;
end
endtask
always @(posedge clk or posedge reset) begin
if (reset) begin
bit_cnt_q <= 4'd0;
frame_q <= 11'd0;
brk_q <= 1'b0;
ext_q <= 1'b0;
ev_valid_o <= 1'b0;
ev_dx_o <= 8'd0;
ev_dy_o <= 8'd0;
ev_btn_o <= 5'd0;
rx_count_o <= 16'd0;
end
else begin
ev_valid_o <= 1'b0;
if (clk_fall) begin
// frame_q[i] = 这一帧的第 i 位:0 = 起始,1..8 = 数据 d0..d7(LSB 先),
// 9 = 校验,10 = 停止。按”位的下标“存,不要再整体右移:
// 右移的话停止位还没进来就先去判 frame_q[10],判到的是校验位。
if (bit_cnt_q == 4'd0) begin
if (ps2_data_s2 == 1'b0) // 起始位有效,开始收
bit_cnt_q <= 4'd1;
end
else begin
frame_q[bit_cnt_q] <= ps2_data_s2;
if (bit_cnt_q == 4'd10) begin
bit_cnt_q <= 4'd0;
// 停止位就是刚采到的这一位(frame_q[10] 本拍还没更新到),
// 校验位查 data + parity 的奇校验
if ((ps2_data_s2 == 1'b1) && parity_ok(frame_q[9:1]))
handle_code(frame_q[8:1]);
end
else
bit_cnt_q <= bit_cnt_q + 4'd1;
end
end
end
end
endmodule
+158
View File
@@ -0,0 +1,158 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// cpu21_ps2_mouse —— 可选的真鼠标输入:PS/2 鼠标(Digilent Pmod PS2 插在 JA 上)。
//
// JA[1] = PS/2 DATA,JA[2] = PS/2 CLK(两条线都需要上拉)。当前 XDC 把 ps2_clk /
// ps2_data 接到板载 USB HID 口的 PS/2 侧(F4 / B2),也可以改回 JA(C17 / D18)。
//
// 注意:接收/解析逻辑(包括帧对齐与奇校验)已由 srcs/tb/tb_ps2_mouse.v 仿真验证;
// 板上那一侧能不能收到数据,取决于板载 USB→PS/2 桥接芯片是否肯转发你插的设备。
// 如果你不用真鼠标,请让顶层用 cpu21_pointer(5 向按键)作为事件源即可,
// C 程序完全不用改。板级顶层 cpu21_board_top 用参数 USE_PS2_MOUSE 选择这两者。
//
// 实现内容:
// 1) 接收:在 ps2_clk 的下降沿采样 ps2_data,收满 11 位(起始 0 + 8 数据
// LSB 先 + 奇校验 + 停止 1)后校验并拼成 3 字节一包;
// frame_q[i] = 这一帧的第 i 位(0 起始、1..8 = d0..d7、9 校验、10 停止),
// 所以字节 = frame_q[8:1](frame_q[1] 就是 d0),校验 = frame_q[9]。
// 注意 PS/2 是**奇校验**:data + parity 9 位里 1 的个数为奇数。
// 2) 解析:byte0 = 标志(bit0 左键、bit1 右键、bit3 固定 1),byte1 = dx,
// byte2 = dy,bit7 为符号位;dx 正 = 右,**dy 正 = 上**(PS/2 的原生方向),
// 而软件里正 dy = 下,所以上报前把 dy 取负,让鼠标和板载按键/键盘方向一致;
// 3) 上报:当 dx/dy 非 0 或按键状态变化时产生一个事件(单位与
// cpu21_pointer 相同,即原始计数 = 1/8 格);
// 4) 本模块只做"接收",不实现主机→鼠标的命令发送(0xF4 使能上报)。多数
// PS/2 鼠标上电后就会主动上报,这类鼠标可以直接用;如果你的鼠标上电毫无
// 反应,说明它需要主机先发 0xF4 —— 那部分需要自行补上,或者直接用默认的
// 5 向按键(cpu21_pointer),C 程序完全不用改。
// -----------------------------------------------------------------------------
module cpu21_ps2_mouse (
input wire clk,
input wire reset,
// PS/2 双向线(开漏)
input wire ps2_clk_i,
output wire ps2_clk_oe_o,
input wire ps2_data_i,
output wire ps2_data_oe_o,
// 事件上报
output reg ev_valid_o,
output reg [7:0] ev_dx_o,
output reg [7:0] ev_dy_o,
output reg [4:0] ev_btn_o,
// 调试:收到的字节数(可选观察)
output reg [15:0] rx_count_o
);
// 开漏:oe = 1 时把线拉低,否则高阻(外部上拉)。本模块只接收,两条线都不驱动。
assign ps2_clk_oe_o = 1'b0;
assign ps2_data_oe_o = 1'b0;
// 两级同步
reg ps2_clk_s1, ps2_clk_s2, ps2_data_s1, ps2_data_s2;
always @(posedge clk 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;
end
else begin
ps2_clk_s1 <= ps2_clk_i;
ps2_clk_s2 <= ps2_clk_s1;
ps2_data_s1 <= ps2_data_i;
ps2_data_s2 <= ps2_data_s1;
end
end
wire clk_fall = ~ps2_clk_s1 & ps2_clk_s2; // 同步后的下降沿
wire clk_rise = ps2_clk_s1 & ~ps2_clk_s2;
// ---------------------------------------------------------------------------
// 接收移位:11 位一帧,bit0 = 起始位,bit1..8 = 数据,bit9 = 校验,bit10 = 停止
// ---------------------------------------------------------------------------
reg [3:0] bit_cnt_q;
reg [10:0] frame_q;
reg frame_ok_q;
reg [23:0] pkt_q; // 3 字节包
reg [1:0] pkt_cnt_q;
reg [7:0] byte_cnt_q; // 一包内的字节序号(0..2)
reg [7:0] dx_q, dy_q;
reg [4:0] btn_q; // {4'b0, middle, right, left} -> 只取低 3 位使用
// 校验:PS/2 是**奇校验**,data 8 位 + parity 位共 9 位里 1 的个数应为奇数
function automatic parity_ok(input [8:0] dp); // dp = {parity, d7..d0}
parity_ok = ^dp;
endfunction
always @(posedge clk or posedge reset) begin
if (reset) begin
bit_cnt_q <= 4'd0;
frame_q <= 11'd0;
frame_ok_q <= 1'b0;
byte_cnt_q <= 8'd0;
pkt_q <= 24'd0;
rx_count_o <= 16'd0;
ev_valid_o <= 1'b0;
end
else begin
ev_valid_o <= 1'b0;
if (clk_fall) begin
// frame_q[i] = 这一帧的第 i 位:0 = 起始,1..8 = 数据 d0..d7(LSB 先),
// 9 = 校验,10 = 停止。按”位的下标“存,不要再整体右移:
// 右移的话停止位还没进来就先去判 frame_q[10],判到的是校验位。
if (bit_cnt_q == 4'd0) begin
if (ps2_data_s2 == 1'b0) // 起始位有效,开始收
bit_cnt_q <= 4'd1;
end
else begin
frame_q[bit_cnt_q] <= ps2_data_s2;
if (bit_cnt_q == 4'd10) begin
bit_cnt_q <= 4'd0;
// 停止位就是刚采到的这一位(frame_q[10] 本拍还没更新到),
// 校验查 data + parity 的奇校验
if ((ps2_data_s2 == 1'b1) && parity_ok(frame_q[9:1])) begin
rx_count_o <= rx_count_o + 16'd1;
case (byte_cnt_q)
8'd0: begin
// 标志字节:bit3 必须为 1(否则是鼠标发来的其他响应,丢弃)
// 数据字节 = frame_q[8:1],它的 bit3 就是 frame_q[4]
// (不能写 frame_q[8:1][3]:Verilog-2001 不允许 part-select 再取位)
if (frame_q[4] == 1'b1) begin
pkt_q[7:0] <= frame_q[8:1];
byte_cnt_q <= 8'd1;
end
end
8'd1: begin
pkt_q[15:8] <= frame_q[8:1]; // dx
byte_cnt_q <= 8'd2;
end
8'd2: begin
byte_cnt_q <= 8'd0;
ev_dx_o <= pkt_q[15:8]; // byte1 = dx(正 = 右,与软件一致)
// byte2 = dy:**PS/2 的正 dy 是鼠标上移**,而软件里正 dy = 光标下移
// (与 cpu21_pointer 的 BTND = +8 一致),所以这里要取负。
ev_dy_o <= 8'd0 - frame_q[8:1];
// 与 cpu21_mmio / mmio.h 一致:bit0=左键, bit1=右键, bit2=中键
ev_btn_o <= {2'b0, pkt_q[2], pkt_q[1], pkt_q[0]};
ev_valid_o <= 1'b1;
end
default:
byte_cnt_q <= 8'd0;
endcase
end
end
else begin
bit_cnt_q <= bit_cnt_q + 4'd1;
end
end
end
end
end
endmodule
`default_nettype wire
@@ -48,8 +48,12 @@ module cpu21_riscv_redirect_int_bpb #(
// MMIO:LED 数据寄存器地址(默认 0x0000_1000,位于 4 KB 数据 RAM 之外)。
parameter [31:0] LED_ADDR = 32'h0000_1000,
// ustatus 复位值。详见 cpu21_riscv_irq_ctrl 中的说明。
parameter [31:0] USTATUS_INIT = 32'h0000_0001
) (
parameter [31:0] USTATUS_INIT = 32'h0000_0001,
// 显存字数:600 字 = 2400 字节 = 40x30 格、每格 2 字节(字形 + 颜色)。
// 必须传给 cpu21_mmio:它的 fb_ok 是 (fb_widx < FB_WORDS),不传就会
// 把显存写卡在前 300 字(前 15 行),屏幕下半部分永远刷不出来。
parameter integer FB_WORDS = 600
) (
input wire clk,
input wire reset,
input wire [31:0] instr_i,
@@ -65,6 +69,24 @@ module cpu21_riscv_redirect_int_bpb #(
output wire [31:0] led_data_o,
output wire led_valid_o,
// ---- 扫雷扩展外设(由 cpu21_mmio 驱动;板级顶层接 VGA/数码管/鼠标)----
output wire [31:0] seg_data_o, // 七段数码管:8 位十六进制数
output wire seg_valid_o,
output wire fb_we_o, // 显存写端口(板级顶层的双口 RAM)
output wire [ 9:0] fb_waddr_o, // 显存字地址(0..599)
output wire [31:0] fb_wdata_o,
output wire [ 3:0] fb_wstrb_o,
input wire [31:0] fb_rdata_i, // 显存读回(组合)
output wire [31:0] disp_ctrl_o, // 显示控制器寄存器(给 VGA)
output wire [31:0] disp_cursor_o,
input wire mouse_ev_valid_i, // 鼠标事件源(板级顶层:按键去抖 / PS/2)
input wire [ 7:0] mouse_dx_i, // 单位 1/8 格,有符号
input wire [ 7:0] mouse_dy_i,
input wire [ 4:0] mouse_btn_i, // {BTNC,BTNU,BTND,BTNL,BTNR}
input wire [15:0] sw_i, // 板载拨码(软件读 INPUT 寄存器)
input wire frame_tick_i, // VGA 每帧一个脉冲
output wire mouse_irq_o, // 鼠标事件中断(板级顶层接 irq_i[0])
output wire level_irq_o, // 软件中断:关卡入口(接 irq_i[1])
output wire [31:0] if_pc_o,
output wire [31:0] id_pc_o,
output wire [31:0] ex_pc_o,
@@ -96,7 +118,7 @@ module cpu21_riscv_redirect_int_bpb #(
output wire [15:0] unconditional_branch_count_o,
output wire [15:0] prediction_success_count_o,
output wire [15:0] prediction_failure_count_o
);
);
// 顶层只用到少量常量,完整译码表见 cpu21_riscv_decoder.v。
localparam OP_BRANCH = 5'h18;
localparam ALU_ADD = 4'd5;
@@ -461,7 +483,8 @@ module cpu21_riscv_redirect_int_bpb #(
wire mmio_rdata_hit;
cpu21_mmio #(
.LED_ADDR(LED_ADDR)
.LED_ADDR(LED_ADDR),
.FB_WORDS(FB_WORDS)
) u_mmio (
.clk (clk),
.reset (reset),
@@ -473,8 +496,30 @@ module cpu21_riscv_redirect_int_bpb #(
.wstrb_i (exmem_wstrb_q),
.rdata_o (mmio_rdata),
.rdata_hit_o(mmio_rdata_hit),
// LED(原样)
.led_data_o (led_data_o),
.led_valid_o(led_valid_o)
.led_valid_o(led_valid_o),
// 七段数码管
.seg_data_o (seg_data_o),
.seg_valid_o(seg_valid_o),
// 显存(本体在板级顶层)
.fb_we_o (fb_we_o),
.fb_waddr_o (fb_waddr_o),
.fb_wdata_o (fb_wdata_o),
.fb_wstrb_o (fb_wstrb_o),
.fb_rdata_i (fb_rdata_i),
// 显示控制器
.disp_ctrl_o (disp_ctrl_o),
.disp_cursor_o(disp_cursor_o),
// 鼠标事件源与中断
.mouse_ev_valid_i(mouse_ev_valid_i),
.mouse_dx_i (mouse_dx_i),
.mouse_dy_i (mouse_dy_i),
.mouse_btn_i (mouse_btn_i),
.sw_i (sw_i),
.frame_tick_i (frame_tick_i),
.mouse_irq_o (mouse_irq_o),
.level_irq_o (level_irq_o)
);
// 访存读数据:命中 MMIO 时用外设数据,否则用外部存储器数据。
@@ -672,9 +717,12 @@ module cpu21_riscv_redirect_int_bpb #(
memwb_wb_sel_q <= 3'b0;
halted_q <= 1'b0;
end else begin
if (go_i) halted_q <= 1'b0;
if (ex_halt_event) halted_q <= 1'b1;
end
else begin
if (go_i)
halted_q <= 1'b0;
if (ex_halt_event)
halted_q <= 1'b1;
// MEM/WB 流水寄存器捕获。
memwb_valid_q <= exmem_valid_q;
@@ -705,7 +753,8 @@ module cpu21_riscv_redirect_int_bpb #(
exmem_mem_write_q <= 1'b0;
exmem_wstrb_q <= 4'b0;
exmem_wb_sel_q <= 3'b0;
end else begin
end
else begin
exmem_valid_q <= idex_valid_q;
exmem_pc_q <= idex_pc_q;
exmem_ir_q <= idex_ir_q;
@@ -768,11 +817,13 @@ module cpu21_riscv_redirect_int_bpb #(
if (halted_q || ex_md_stall) begin
// 停机 / 乘除法迭代:valid 保持不变(指令继续占用 EX)。
end else if (pipeline_stall || flush_younger) begin
end
else if (pipeline_stall || flush_younger) begin
// load-use 停顿:load 已经进 EX/MEM,这里给 EX 装一个气泡(payload 保持
// 原值,valid=0 时无害);冲刷同理。
idex_valid_q <= 1'b0;
end else begin
end
else begin
idex_valid_q <= ifid_valid_q;
end
@@ -781,32 +832,38 @@ module cpu21_riscv_redirect_int_bpb #(
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end else if (ex_uret_redirect) begin
end
else if (ex_uret_redirect) begin
pc_q <= ex_return_pc;
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end else if (ex_halt_event) begin
end
else if (ex_halt_event) begin
pc_q <= idex_pc_q + 32'd4;
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end else if (ex_redirect_valid) begin
end
else if (ex_redirect_valid) begin
pc_q <= ex_redirect_pc;
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end else if (pipeline_stall || ex_md_stall) begin
end
else if (pipeline_stall || ex_md_stall) begin
pc_q <= pc_q;
ifid_valid_q <= ifid_valid_q;
ifid_pc_q <= ifid_pc_q;
ifid_ir_q <= ifid_ir_q;
ifid_pred_taken_q <= ifid_pred_taken_q;
ifid_pred_target_q <= ifid_pred_target_q;
end else if (halted_q) begin
end
else if (halted_q) begin
pc_q <= pc_q;
ifid_valid_q <= 1'b0;
end else begin
end
else begin
pc_q <= fetch_next_pc;
ifid_valid_q <= 1'b1;
ifid_pc_q <= pc_q;
+107
View File
@@ -0,0 +1,107 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// cpu21_seg7 —— Nexys4DDR 板载 8 位数码管的动态扫描驱动。
//
// 输入:seg_data_i 每 4 位一个十六进制数字(低 4 位 = 最右边那个数码管),
// an_en_i 每一位对应一个数码管的使能(1 = 显示,0 = 熄灭)。
// 输出:an_o[7:0] 位选(低有效)、seg_o[6:0] 段选 a..g(低有效)、dp_o 小数点。
//
// 扫描频率:REFRESH_DIV 分频后约 1 kHz 切换一位(8 位 => 每位数码管 125 Hz
// 刷新)。REFRESH_DIV 由顶层按 CPU 时钟频率给出(默认 50000 @50 MHz),
// 这样 CPU 降频时扫描刷新率不会跟着变慢。
// 板载数码管是共阳极:位选拉低选中,段选拉低点亮。
// -----------------------------------------------------------------------------
module cpu21_seg7 #(
parameter integer REFRESH_DIV = 50000 // 50 MHz / 50000 = 1 kHz 切换一位
) (
input wire clk,
input wire reset,
input wire [31:0] seg_data_i, // 8 个十六进制数字
input wire [ 7:0] an_en_i, // 每位是否显示
output reg [ 7:0] an_o, // 位选,低有效
output reg [ 6:0] seg_o, // 段选 a..g,低有效
output reg dp_o // 小数点,低有效
);
// ---------------------------------------------------------------------------
// 刷新计数与当前位
// ---------------------------------------------------------------------------
// 计数位宽要能装下 REFRESH_DIV-1:顶层按 CPU 频率等比给出该值
//(50 MHz 时 50000,12.5 MHz 时 12500;若不分频直接跑 100 MHz 则 100000),
// 因此这里用 20 位(最大约 100 万)留足余量。
reg [19:0] div_q;
reg [ 2:0] digit_q;
wire tick = (div_q == (REFRESH_DIV - 1));
always @(posedge clk or posedge reset) begin
if (reset) begin
div_q <= 20'd0;
digit_q <= 3'd0;
end else if (tick) begin
div_q <= 20'd0;
digit_q <= digit_q + 3'd1;
end else begin
div_q <= div_q + 16'd1;
end
end
// 当前要显示的十六进制数字
reg [3:0] nib;
always @* begin
case (digit_q)
3'd0: nib = seg_data_i[3:0];
3'd1: nib = seg_data_i[7:4];
3'd2: nib = seg_data_i[11:8];
3'd3: nib = seg_data_i[15:12];
3'd4: nib = seg_data_i[19:16];
3'd5: nib = seg_data_i[23:20];
3'd6: nib = seg_data_i[27:24];
default: nib = seg_data_i[31:28];
endcase
end
// ---------------------------------------------------------------------------
// 十六进制 -> 七段码(bit0=a ... bit6=g,1 表示该段点亮)
// ---------------------------------------------------------------------------
reg [6:0] seg_on;
always @* begin
case (nib)
4'h0: seg_on = 7'b0111111;
4'h1: seg_on = 7'b0000110;
4'h2: seg_on = 7'b1011011;
4'h3: seg_on = 7'b1001111;
4'h4: seg_on = 7'b1100110;
4'h5: seg_on = 7'b1101101;
4'h6: seg_on = 7'b1111101;
4'h7: seg_on = 7'b0000111;
4'h8: seg_on = 7'b1111111;
4'h9: seg_on = 7'b1101111;
4'hA: seg_on = 7'b1110111;
4'hB: seg_on = 7'b1111100;
4'hC: seg_on = 7'b0111001;
4'hD: seg_on = 7'b1011110;
4'hE: seg_on = 7'b1111001;
default: seg_on = 7'b1110001; // F
endcase
end
// ---------------------------------------------------------------------------
// 位选与段选输出(共阳极 => 低有效)
// ---------------------------------------------------------------------------
always @(posedge clk or posedge reset) begin
if (reset) begin
an_o <= 8'hFF;
seg_o <= 7'h7F;
dp_o <= 1'b1;
end else if (tick) begin
an_o <= ~(8'b0000_0001 << digit_q) | ~an_en_i;
seg_o <= ~seg_on;
dp_o <= 1'b1; // 不用小数点
end
end
endmodule
`default_nettype wire
+328
View File
@@ -0,0 +1,328 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// cpu21_vga —— 640x480@60 的"格子"显示控制器(显存端口接板级顶层的双口 RAM)。
//
// CPU 不直接操作像素:它只通过 MMIO 往显示缓冲区里写"每格 2 字节"(字形 + 颜色),
// 本模块按像素扫描把格子展开成画面(8x8 字模放大 2 倍填满 16x16 的格子)。
// 这样一格只要 1 次字节写,刷屏开销极小。
//
// 网格:40 列 x 30 行,每格 16x16 像素 = 640x480。
//
// 时钟:本模块工作在 CPU 时钟(clk)下,用 tick(每 2 个 clk 一个使能)产生
// 25 MHz 的像素节拍。**不引入第二个时钟域**,因此显示缓冲区的读(本模块)
// 与写(MMIO,同一时钟)之间没有跨时钟域问题。
//
// 光标:由 CURSOR 寄存器的 (x,y) 决定,在对应格子上画一个黄色常亮的方框(2 像素宽)
// (CTRL[0] 控制显示与否)。缓冲区字节的 bit7 在本模块中不使用(保留)。
// -----------------------------------------------------------------------------
module cpu21_vga #(
parameter integer H_VISIBLE = 640,
parameter integer H_FP = 16,
parameter integer H_SYNC = 96,
parameter integer H_BP = 48,
parameter integer V_VISIBLE = 480,
parameter integer V_FP = 10,
parameter integer V_SYNC = 2,
parameter integer V_BP = 33,
parameter integer COLS = 40,
parameter integer ROWS = 30
) (
input wire clk,
input wire reset,
input wire tick, // 25 MHz 像素使能(每 2 个 clk 一拍)
// 显示缓冲区读端口
output wire [ 9:0] fb_raddr_o,
input wire [31:0] fb_word_i,
// 控制
input wire [31:0] ctrl_i,
input wire [31:0] cursor_i,
// VGA 输出
output reg [ 3:0] vga_r,
output reg [ 3:0] vga_g,
output reg [ 3:0] vga_b,
output reg vga_hs,
output reg vga_vs,
output reg frame_tick_o
);
localparam integer H_TOTAL = H_VISIBLE + H_FP + H_SYNC + H_BP; // 800
localparam integer V_TOTAL = V_VISIBLE + V_FP + V_SYNC + V_BP; // 525
// ---------------------------------------------------------------------------
// 扫描计数
// ---------------------------------------------------------------------------
reg [9:0] hx, vy;
always @(posedge clk or posedge reset) begin
if (reset) begin
hx <= 10'd0;
vy <= 10'd0;
end else if (tick) begin
if (hx == H_TOTAL - 1) begin
hx <= 10'd0;
vy <= (vy == V_TOTAL - 1) ? 10'd0 : (vy + 10'd1);
end else begin
hx <= hx + 10'd1;
end
end
end
wire de = (hx < H_VISIBLE) && (vy < V_VISIBLE);
// 同步信号在 tick 上寄存输出
wire hs_n = ~((hx >= (H_VISIBLE + H_FP)) && (hx < (H_VISIBLE + H_FP + H_SYNC)));
wire vs_n = ~((vy >= (V_VISIBLE + V_FP)) && (vy < (V_VISIBLE + V_FP + V_SYNC)));
// ---------------------------------------------------------------------------
// 格子索引与缓冲区读出
// ---------------------------------------------------------------------------
wire [5:0] ccol = hx[9:4]; // 0..39
wire [4:0] crow = vy[8:4]; // 0..29
wire [10:0] cidx = (crow << 5) + (crow << 3) + ccol; // row*40 + col
assign fb_raddr_o = cidx[10:1]; // 字地址(1 字 = 2 格)
wire [ 1:0] lane = {cidx[0], 1'b0};
wire [ 1:0] color_lane = lane + 2'd1;
wire [31:0] cell_word = fb_word_i;
wire [ 7:0] glyph_byte = cell_word >> {lane, 3'b000};
wire [ 7:0] color_byte = cell_word >> {color_lane, 3'b000};
wire [ 5:0] glyph = glyph_byte[5:0];
wire [ 2:0] gcolor = color_byte[2:0];
// 全局背景色(字模点为 0 时显示的颜色)
wire [ 2:0] bgcolor = ctrl_i[7:4];
// ---------------------------------------------------------------------------
// 8x8 字模(bit7 = 最左像素),放大 2 倍填满 16x16 的格子;字形编号 0..63。
// ---------------------------------------------------------------------------
wire [ 2:0] fx = hx[3:1];
wire [ 2:0] fy = vy[3:1];
function [7:0] font_row;
input [5:0] g;
input [2:0] r;
begin
case (g)
6'd0: font_row = 8'h00; // 空白
6'd1:
font_row = (r == 3'd0) ? 8'h18 : (r == 3'd1) ? 8'h38 : (r == 3'd2) ? 8'h18 :
(r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h18 :
(r == 3'd6) ? 8'h7E : 8'h00; // 1
6'd2:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h06 :
(r == 3'd3) ? 8'h0C : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h30 :
(r == 3'd6) ? 8'h7E : 8'h00; // 2
6'd3:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h06 :
(r == 3'd3) ? 8'h1C : (r == 3'd4) ? 8'h06 : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h3C : 8'h00; // 3
6'd4:
font_row = (r == 3'd0) ? 8'h0C : (r == 3'd1) ? 8'h1C : (r == 3'd2) ? 8'h3C :
(r == 3'd3) ? 8'h6C : (r == 3'd4) ? 8'h7E : (r == 3'd5) ? 8'h0C :
(r == 3'd6) ? 8'h0C : 8'h00; // 4
6'd5:
font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h60 : (r == 3'd2) ? 8'h7C :
(r == 3'd3) ? 8'h06 : (r == 3'd4) ? 8'h06 : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h3C : 8'h00; // 5
6'd6:
font_row = (r == 3'd0) ? 8'h1C : (r == 3'd1) ? 8'h30 : (r == 3'd2) ? 8'h60 :
(r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h3C : 8'h00; // 6
6'd7:
font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h0C :
(r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h18 :
(r == 3'd6) ? 8'h18 : 8'h00; // 7
6'd8:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 :
(r == 3'd3) ? 8'h3C : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h3C : 8'h00; // 8
6'd9:
font_row = (r == 3'd0) ? 8'hFF : (r == 3'd1) ? 8'h81 : (r == 3'd2) ? 8'hBD :
(r == 3'd3) ? 8'hA5 : (r == 3'd4) ? 8'hA5 : (r == 3'd5) ? 8'hBD :
(r == 3'd6) ? 8'h81 : 8'hFF; // 未揭开的方块
6'd10:
font_row = (r == 3'd0) ? 8'h40 : (r == 3'd1) ? 8'h70 : (r == 3'd2) ? 8'h78 :
(r == 3'd3) ? 8'h70 : (r == 3'd4) ? 8'h40 : (r == 3'd5) ? 8'h40 :
(r == 3'd6) ? 8'h40 : 8'h00; // 旗
6'd11:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h7E : (r == 3'd2) ? 8'hFF :
(r == 3'd3) ? 8'hFF : (r == 3'd4) ? 8'hFF : (r == 3'd5) ? 8'hFF :
(r == 3'd6) ? 8'h7E : 8'h3C; // 雷
6'd12:
font_row = (r == 3'd0) ? 8'h81 : (r == 3'd1) ? 8'h42 : (r == 3'd2) ? 8'h24 :
(r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h24 :
(r == 3'd6) ? 8'h42 : 8'h81; // 爆炸
6'd13:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h06 :
(r == 3'd3) ? 8'h0C : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h00 :
(r == 3'd6) ? 8'h18 : 8'h00; // 问号
6'd14: font_row = (r == 3'd3) ? 8'hFF : (r == 3'd4) ? 8'hFF : 8'h00; // 横线
6'd15: font_row = 8'hFF; // 实心
6'd16:
font_row = (r == 3'd0) ? 8'h18 : (r == 3'd1) ? 8'h3C : (r == 3'd2) ? 8'h66 :
(r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h7E : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h66 : 8'h00; // A
6'd17:
font_row = (r == 3'd0) ? 8'h7C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 :
(r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h7C : 8'h00; // B
6'd18:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h60 :
(r == 3'd3) ? 8'h60 : (r == 3'd4) ? 8'h60 : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h3C : 8'h00; // C
6'd19:
font_row = (r == 3'd0) ? 8'h78 : (r == 3'd1) ? 8'h6C : (r == 3'd2) ? 8'h66 :
(r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h6C :
(r == 3'd6) ? 8'h78 : 8'h00; // D
6'd20:
font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h60 : (r == 3'd2) ? 8'h60 :
(r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h60 : (r == 3'd5) ? 8'h60 :
(r == 3'd6) ? 8'h7E : 8'h00; // E
6'd21:
font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h60 : (r == 3'd2) ? 8'h60 :
(r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h60 : (r == 3'd5) ? 8'h60 :
(r == 3'd6) ? 8'h60 : 8'h00; // F
6'd22:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h60 :
(r == 3'd3) ? 8'h6E : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h3E : 8'h00; // G
6'd23:
font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 :
(r == 3'd3) ? 8'h7E : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h66 : 8'h00; // H
6'd24:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h18 : (r == 3'd2) ? 8'h18 :
(r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h18 :
(r == 3'd6) ? 8'h3C : 8'h00; // I
6'd25:
font_row = (r == 3'd0) ? 8'h1E : (r == 3'd1) ? 8'h0C : (r == 3'd2) ? 8'h0C :
(r == 3'd3) ? 8'h0C : (r == 3'd4) ? 8'h0C : (r == 3'd5) ? 8'h6C :
(r == 3'd6) ? 8'h38 : 8'h00; // J
6'd26:
font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h6C : (r == 3'd2) ? 8'h78 :
(r == 3'd3) ? 8'h70 : (r == 3'd4) ? 8'h78 : (r == 3'd5) ? 8'h6C :
(r == 3'd6) ? 8'h66 : 8'h00; // K
6'd27:
font_row = (r == 3'd0) ? 8'h60 : (r == 3'd1) ? 8'h60 : (r == 3'd2) ? 8'h60 :
(r == 3'd3) ? 8'h60 : (r == 3'd4) ? 8'h60 : (r == 3'd5) ? 8'h60 :
(r == 3'd6) ? 8'h7E : 8'h00; // L
6'd28:
font_row = (r == 3'd0) ? 8'h63 : (r == 3'd1) ? 8'h77 : (r == 3'd2) ? 8'h7F :
(r == 3'd3) ? 8'h6B : (r == 3'd4) ? 8'h63 : (r == 3'd5) ? 8'h63 :
(r == 3'd6) ? 8'h63 : 8'h00; // M
6'd29:
font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h76 : (r == 3'd2) ? 8'h7E :
(r == 3'd3) ? 8'h6E : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h66 : 8'h00; // N
6'd30:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 :
(r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h3C : 8'h00; // O
6'd31:
font_row = (r == 3'd0) ? 8'h7C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 :
(r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h60 : (r == 3'd5) ? 8'h60 :
(r == 3'd6) ? 8'h60 : 8'h00; // P
6'd32:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 :
(r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h6E : (r == 3'd5) ? 8'h3C :
(r == 3'd6) ? 8'h0E : 8'h00; // Q
6'd33:
font_row = (r == 3'd0) ? 8'h7C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 :
(r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h78 : (r == 3'd5) ? 8'h6C :
(r == 3'd6) ? 8'h66 : 8'h00; // R
6'd34:
font_row = (r == 3'd0) ? 8'h3E : (r == 3'd1) ? 8'h60 : (r == 3'd2) ? 8'h60 :
(r == 3'd3) ? 8'h3C : (r == 3'd4) ? 8'h06 : (r == 3'd5) ? 8'h06 :
(r == 3'd6) ? 8'h7C : 8'h00; // S
6'd35:
font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h18 : (r == 3'd2) ? 8'h18 :
(r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h18 :
(r == 3'd6) ? 8'h18 : 8'h00; // T
6'd36:
font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 :
(r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h3C : 8'h00; // U
6'd37:
font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 :
(r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h3C :
(r == 3'd6) ? 8'h18 : 8'h00; // V
6'd38:
font_row = (r == 3'd0) ? 8'h63 : (r == 3'd1) ? 8'h63 : (r == 3'd2) ? 8'h63 :
(r == 3'd3) ? 8'h6B : (r == 3'd4) ? 8'h7F : (r == 3'd5) ? 8'h77 :
(r == 3'd6) ? 8'h63 : 8'h00; // W
6'd39:
font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h3C :
(r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h3C : (r == 3'd5) ? 8'h66 :
(r == 3'd6) ? 8'h66 : 8'h00; // X
6'd40:
font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h3C :
(r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h18 :
(r == 3'd6) ? 8'h18 : 8'h00; // Y
6'd41:
font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h06 : (r == 3'd2) ? 8'h0C :
(r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h30 : (r == 3'd5) ? 8'h60 :
(r == 3'd6) ? 8'h7E : 8'h00; // Z
default: font_row = 8'h00;
endcase
end
endfunction
wire [7:0] frow = font_row(glyph, fy);
wire glyph_px = frow[3'd7-fx]; // 1 = 画字模点
// ---------------------------------------------------------------------------
// 光标框:黄色、常亮、2 像素宽
// 原来是 1 像素浅灰 + 闪动(blink 周期约 1.3 s):在白色未揭开格上几乎看不清,
// 而且截图有一半概率正好截到"灭"的那个相位。改成黄色常亮 2 像素后,
// VGA 上和图里都能一眼看到指针。想改回闪动:把 blink_q 加回来,
// 再在 cursor_box 里与上 blink 即可。
// ---------------------------------------------------------------------------
// 列必须比 6 位(0..39):hard 难度的棋盘宽 30,光标能到列 35,
// 以前这里只取 [4:0],35 被当成 3,光标会“跳回最左边”。
wire cursor_here = (cursor_i[5:0] == ccol) && (cursor_i[12:8] == crow);
wire on_border = (hx[3:0] < 4'd2) || (hx[3:0] > 4'd13) || (vy[3:0] < 4'd2) || (vy[3:0] > 4'd13);
wire cursor_box = cursor_here && ctrl_i[0] && on_border;
// ---------------------------------------------------------------------------
// 颜色(3 位调色板 -> 4:4:4 RGB)
// ---------------------------------------------------------------------------
function [11:0] pal;
input [2:0] c;
begin
case (c)
3'd0: pal = {4'h0, 4'h0, 4'h0}; // 黑
3'd1: pal = {4'h0, 4'h0, 4'hA}; // 深蓝
3'd2: pal = {4'h0, 4'hA, 4'h0}; // 绿
3'd3: pal = {4'h0, 4'hA, 4'hA}; // 青
3'd4: pal = {4'hA, 4'h0, 4'h0}; // 红
3'd5: pal = {4'hA, 4'h0, 4'hA}; // 洋红
3'd6: pal = {4'hA, 4'hA, 4'h0}; // 黄
default: pal = {4'hA, 4'hA, 4'hA}; // 浅灰
endcase
end
endfunction
wire [ 2:0] px_color = glyph_px ? gcolor : bgcolor;
wire [ 2:0] px_final = cursor_box ? 3'd6 : px_color; // 6 = 黄,光标框
wire [11:0] px_rgb = de ? pal(px_final) : 12'h000;
always @(posedge clk or posedge reset) begin
if (reset) begin
{vga_r, vga_g, vga_b} <= 12'h000;
vga_hs <= 1'b1;
vga_vs <= 1'b1;
frame_tick_o <= 1'b0;
end else if (tick) begin
{vga_r, vga_g, vga_b} <= px_rgb;
vga_hs <= hs_n;
vga_vs <= vs_n;
frame_tick_o <= (hx == H_TOTAL - 1) && (vy == V_TOTAL - 1);
end
end
endmodule
`default_nettype wire
+19 -1
View File
@@ -30,7 +30,25 @@ module tb_board_top;
.go (go),
.irq_sw(irq_sw),
.led (led),
.led_we(led_we)
.led_we(led_we),
// 扫雷扩展端口:本测试平台不复位测试这些功能,输入显式接 0/1,
// 避免悬空(z)传到中断控制器里影响跑马灯的比对结果。
.btnu (1'b0),
.btnd (1'b0),
.btnl (1'b0),
.btnr (1'b0),
.sw_flag (1'b0),
.hid_sel (1'b0),
.ps2_clk (1'b1),
.ps2_data(1'b1),
.an (),
.seg (),
.dp (),
.vga_r (),
.vga_g (),
.vga_b (),
.vga_hs (),
.vga_vs ()
);
integer changes = 0;
+169
View File
@@ -0,0 +1,169 @@
`timescale 1ns / 1ps
// =============================================================================
// tb_ps2_kbd —— 单独验证 PS/2 键盘解码(扫描码集 2)
//
// 发 → 键(E0 74)应产生 dx=+8;发 ↑(E0 75)应产生 dy=-8;
// 发 Enter(5A)应产生 btn=1(开格);发 左Shift(12)应产生 btn=2(插旗);
// 发 W(1D)应产生 dy=-8(W/A/S/D 别名);
// 松开码(F0 xx)不应产生事件;校验位错的帧应被整帧丢掉。
//
// PS/2 键盘发扫描码(集 2):0xF0 前缀 = 下一个码是"松开",0xE0 前缀 = 扩展键,
// 方向键是扩展键(↑ = E0 75、↓ = E0 72、← = E0 6B、→ = E0 74),
// 所以解码器只对"按下"产生事件。键盘自动重复 = 按住方向键时连续移动。
//
// 跑法:
// xvlog srcs/rtl/cpu21_ps2_kbd.v srcs/tb/tb_ps2_kbd.v
// xelab -top tb_ps2_kbd -snapshot kbdsim
// xsim kbdsim -R
// =============================================================================
module tb_ps2_kbd;
reg clk = 1'b0;
reg reset = 1'b1;
reg ps2_clk = 1'b1;
reg ps2_data = 1'b1;
wire ev_valid;
wire [7:0] ev_dx, ev_dy;
wire [4:0] ev_btn;
wire [15:0] rx_count;
always #10 clk = ~clk; // 50 MHz
cpu21_ps2_kbd dut (
.clk (clk),
.reset (reset),
.ps2_clk_i (ps2_clk),
.ps2_data_i (ps2_data),
.ev_valid_o (ev_valid),
.ev_dx_o (ev_dx),
.ev_dy_o (ev_dy),
.ev_btn_o (ev_btn),
.rx_count_o (rx_count)
);
// 发一个 PS/2 位:先摆数据,稳定后拉低时钟(DUT 在下降沿采样)
task send_bit;
input b;
begin
ps2_data = b;
#(10_000);
ps2_clk = 1'b0;
#(20_000);
ps2_clk = 1'b1;
#(20_000);
end
endtask
// 发一个字节:起始 0 + 8 位数据(LSB first) + 奇校验 + 停止 1
task send_byte;
input [7:0] d;
integer i;
reg p;
begin
p = ~^d; // 奇校验
send_bit(1'b0);
for (i = 0; i < 8; i = i + 1)
send_bit(d[i]);
send_bit(p);
send_bit(1'b1);
end
endtask
// 校验位错的字节(应被整帧丢掉)
task send_byte_bad_parity;
input [7:0] d;
integer i;
begin
send_bit(1'b0);
for (i = 0; i < 8; i = i + 1)
send_bit(d[i]);
send_bit(^d);
send_bit(1'b1);
end
endtask
integer n_ev = 0;
integer ok = 1;
integer exp_dx, exp_dy, exp_btn;
always @(posedge clk) begin
if (ev_valid) begin
n_ev = n_ev + 1;
$display(" [kbd] event #%0d: dx=%0d dy=%0d btn=%b", n_ev, $signed(ev_dx), $signed(ev_dy), ev_btn);
if ((exp_dx != 999) && ($signed(ev_dx) != exp_dx)) begin
$display(" [kbd] FAIL: 期望 dx=%0d", exp_dx);
ok = 0;
end
if ((exp_dy != 999) && ($signed(ev_dy) != exp_dy)) begin
$display(" [kbd] FAIL: 期望 dy=%0d", exp_dy);
ok = 0;
end
if ((exp_btn != 999) && (ev_btn != exp_btn)) begin
$display(" [kbd] FAIL: 期望 btn=%b", exp_btn);
ok = 0;
end
end
end
initial begin
$display("[kbd] === PS/2 keyboard decode test ===");
#2000;
reset = 1'b0;
#2000;
// 校验错的字节(0x74)不能被当成"→"键
$display("[kbd] 校验位错的字节应该被丢掉");
exp_dx = 999; exp_dy = 999; exp_btn = 999;
send_byte_bad_parity(8'h74);
#(100_000);
if (n_ev != 0) begin
$display(" [kbd] FAIL: 校验错的帧不该产生事件");
ok = 0;
end
$display("[kbd] 只发松开码 F0 75(不该有事件)");
send_byte(8'hF0);
send_byte(8'h75);
#(100_000);
if (n_ev != 0) begin
$display(" [kbd] FAIL: 松开码不该产生事件");
ok = 0;
end
$display("[kbd] → 键(E0 74)期望 dx=+8");
exp_dx = 8; exp_dy = 0; exp_btn = 0;
send_byte(8'hE0);
send_byte(8'h74);
#(100_000);
$display("[kbd] ↑ 键(E0 75)期望 dy=-8");
exp_dx = 0; exp_dy = -8; exp_btn = 0;
send_byte(8'hE0);
send_byte(8'h75);
#(100_000);
$display("[kbd] Enter(5A)期望 btn=00001");
exp_dx = 0; exp_dy = 0; exp_btn = 5'b00001;
send_byte(8'h5A);
#(100_000);
$display("[kbd] 左 Shift(12)期望 btn=00010(软件里 = 插旗)");
exp_dx = 0; exp_dy = 0; exp_btn = 5'b00010;
send_byte(8'h12);
#(100_000);
$display("[kbd] W 键(1D)期望 dy=-8(W/A/S/D 是方向键别名)");
exp_dx = 0; exp_dy = -8; exp_btn = 0;
send_byte(8'h1D);
#(100_000);
$display("[kbd] rx_count=%0d events=%0d", rx_count, n_ev);
if ((ok == 0) || (n_ev != 5))
$display("[kbd] === FAIL ===");
else
$display("[kbd] === ALL PASS ===");
$finish;
end
endmodule
+153
View File
@@ -0,0 +1,153 @@
`timescale 1ns / 1ps
// =============================================================================
// tb_ps2_mouse —— 单独验证 PS/2 鼠标接收逻辑(不跑 CPU,几秒出结果)
//
// 模拟 PS/2 鼠标发 3 字节包:
// byte0 = 标志字节(bit3 必须为 1 才是有效的鼠标数据;bit0/1/2 = 左/右/中键)
// byte1 = dx(有符号) byte2 = dy(有符号)
// 检查 cpu21_ps2_mouse 是否给出正确的 ev_valid / ev_dx / ev_dy / ev_btn。
//
// PS/2 时序:DUT 在 ps2_clk 的**下降沿**采样 ps2_data,所以这里先把数据摆好,
// 再拉低时钟。一帧 11 位 = 起始 0 + 8 位数据(LSB 先) + **奇校验** + 停止 1。
// 奇校验 = 校验位让 data + parity 共 9 位里 1 的个数为奇数(真设备就是这么发的)。
//
// 方向约定:PS/2 的 **dx 正 = 右、dy 正 = 上**(真鼠标往上推,byte2 是正数);
// 而软件(saolei.c / cpu21_pointer)里 **dy 正 = 向下(行号增加)**,
// 所以 cpu21_ps2_mouse 上报前把 dy 取负 —— 本测试就是盯这个约定:
// 发“向上”的 dy,事件里必须得到负的 dy。
//
// 跑法(Vivado 的 xvlog/xelab/xsim 或其它仿真器都行):
// xvlog srcs/rtl/cpu21_ps2_mouse.v srcs/tb/tb_ps2_mouse.v
// xelab -top tb_ps2_mouse -snapshot ps2sim
// xsim ps2sim -R
// =============================================================================
module tb_ps2_mouse;
reg clk = 1'b0;
reg reset = 1'b1;
reg ps2_clk = 1'b1;
reg ps2_data = 1'b1;
wire ev_valid;
wire [7:0] ev_dx;
wire [7:0] ev_dy;
wire [4:0] ev_btn;
wire [15:0] rx_count;
always #10 clk = ~clk; // 50 MHz,和板上 CPU 主频一致
cpu21_ps2_mouse dut (
.clk (clk),
.reset (reset),
.ps2_clk_i (ps2_clk),
.ps2_clk_oe_o (),
.ps2_data_i (ps2_data),
.ps2_data_oe_o(),
.ev_valid_o (ev_valid),
.ev_dx_o (ev_dx),
.ev_dy_o (ev_dy),
.ev_btn_o (ev_btn),
.rx_count_o (rx_count)
);
// 发一个 PS/2 位:先摆数据,稳定后再拉低时钟(DUT 在下降沿采样)
task send_bit;
input b;
begin
ps2_data = b;
#(10_000);
ps2_clk = 1'b0;
#(20_000);
ps2_clk = 1'b1;
#(20_000);
end
endtask
// 发一个字节:起始 0 + 8 位数据(LSB first) + 奇校验 + 停止 1
task send_byte;
input [7:0] d;
integer i;
reg p;
begin
p = ~^d; // 奇校验:data + parity 里 1 的个数为奇数
send_bit(1'b0);
for (i = 0; i < 8; i = i + 1)
send_bit(d[i]);
send_bit(p);
send_bit(1'b1);
end
endtask
// 故意发一个校验位错的字节(用于确认校验真的在起作用)
task send_byte_bad_parity;
input [7:0] d;
integer i;
begin
send_bit(1'b0);
for (i = 0; i < 8; i = i + 1)
send_bit(d[i]);
send_bit(^d); // 错误的校验位
send_bit(1'b1);
end
endtask
integer n_ev = 0;
integer ok = 1;
always @(posedge clk) begin
if (ev_valid) begin
n_ev = n_ev + 1;
$display(" [ps2] event #%0d: dx=%0d dy=%0d btn=%b", n_ev, $signed(ev_dx), $signed(ev_dy), ev_btn);
if (n_ev == 1) begin
if (($signed(ev_dx) != 8) || ($signed(ev_dy) != -8) || (ev_btn != 5'b00000)) begin
$display(" [ps2] FAIL: 第 1 包应为 dx=+8 dy=-8 btn=00000(鼠标向上 -> 事件 dy 为负)");
ok = 0;
end
end
if (n_ev == 2) begin
if (($signed(ev_dx) != -3) || ($signed(ev_dy) != 5) || (ev_btn[0] != 1'b1)) begin
$display(" [ps2] FAIL: 第 2 包应为 dx=-3 dy=+5 btn=xxxx1(鼠标向下 -> 事件 dy 为正)");
ok = 0;
end
end
end
end
integer rx_after_bad;
initial begin
$display("[ps2] === PS/2 mouse receiver test ===");
#2000;
reset = 1'b0;
#2000;
$display("[ps2] 校验位错的字节应该被丢掉");
send_byte_bad_parity(8'h08);
#(100_000);
if (rx_count != 16'd0) begin
$display(" [ps2] FAIL: 校验错的帧不该被计数(rx_count=%0d)", rx_count);
ok = 0;
end
rx_after_bad = rx_count;
$display("[ps2] packet 1: flag=0x08(no button) dx=+8 dy=+8(鼠标向右上移)");
send_byte(8'h08);
send_byte(8'h08);
send_byte(8'h08);
#(100_000);
$display("[ps2] packet 2: flag=0x09(left) dx=-3 dy=-5(向左下移)");
send_byte(8'h09);
send_byte(8'hFD);
send_byte(8'hFB);
#(100_000);
$display("[ps2] rx_count=%0d events=%0d", rx_count, n_ev);
if ((rx_count != rx_after_bad + 16'd6) || (n_ev != 2) || (ok == 0))
$display("[ps2] === FAIL ===");
else
$display("[ps2] === ALL PASS ===");
$finish;
end
endmodule
+464
View File
@@ -0,0 +1,464 @@
`timescale 1ns / 1ps
// =============================================================================
// tb_saolei —— 没有 VGA 显示器时的"扫雷调试台"
//
// 做法:跑真实的板级顶层 cpu21_board_top(宏 SAOLEI_ROM = 扫雷程序),用 5 向按键
// 模拟鼠标(板上 BTNC = 左键、BTNU/D/L/R = 上下左右,R15=SW3 = 插旗模式),
// 定期把显存 1200 字节 dump 成文本;再由 saolei/tools/fb2png.py 按 cpu21_vga.v
// 里的 8x8 字库与 3 位调色板还原成 PNG —— 等价于给 VGA 画面"截图"。
//
// 编译(注意两个 -d 都要带 SAOLEI_ROM):
// xvlog -d SAOLEI_ROM -i srcs/board -i srcs/rtl srcs/rtl/*.v \
// srcs/board/cpu21_board_top.v srcs/tb/tb_saolei.v
// xelab -d SAOLEI_ROM -top tb_saolei -snapshot saolei_sim
// xsim saolei_sim -tclbatch run_all.tcl
//
// 说明:为了在仿真里按键快一点,用 defparam 把 cpu21_pointer 的去抖采样周期
// 从 50000 个 CPU 周期(1 ms @50 MHz)压到 50(1 us),这样每次"按键"只要几微秒
// 就能产生一个事件;上板时那个参数仍然是 50000,行为不变。
// =============================================================================
module tb_saolei;
// ---- 板级顶层端口 ----
reg clk100 = 1'b0;
reg rst_n = 1'b0;
reg go = 1'b0;
reg [ 2:0] irq_sw = 3'b000;
reg btnu = 1'b0;
reg btnd = 1'b0;
reg btnl = 1'b0;
reg btnr = 1'b0;
reg sw_flag = 1'b0;
wire [15:0] led;
wire led_we;
wire [ 7:0] an;
wire [ 6:0] seg;
wire dp;
wire [ 3:0] vga_r, vga_g, vga_b;
wire vga_hs, vga_vs;
// 100 MHz 参考时钟
always #5 clk100 = ~clk100;
cpu21_board_top #(
.POINTER_SAMPLE_DIV(50) // 仿真里加速按键去抖(上板默认 50000)
) dut (
.clk100 (clk100),
.rst_n (rst_n),
.go (go),
.irq_sw (irq_sw),
.btnu (btnu),
.btnd (btnd),
.btnl (btnl),
.btnr (btnr),
.sw_flag (sw_flag),
.led (led),
.led_we (led_we),
.an (an),
.seg (seg),
.dp (dp),
.vga_r (vga_r),
.vga_g (vga_g),
.vga_b (vga_b),
.vga_hs (vga_hs),
.vga_vs (vga_vs),
.ps2_clk (1'b1),
.ps2_data(1'b1)
);
// (按键去抖已通过上面的 POINTER_SAMPLE_DIV(50) 参数加速,不用 defparam)
// ---------------------------------------------------------------- 按键
// which: 0=go(左键) 1=btnu 2=btnd 3=btnl 4=btnr
task press;
input integer which;
input integer us; // 按住时长(微秒)
begin
case (which)
0:
go = 1'b1;
1:
btnu = 1'b1;
2:
btnd = 1'b1;
3:
btnl = 1'b1;
4:
btnr = 1'b1;
endcase
#(us * 1000);
case (which)
0:
go = 1'b0;
1:
btnu = 1'b0;
2:
btnd = 1'b0;
3:
btnl = 1'b0;
4:
btnr = 1'b0;
endcase
#(200 * 1000); // 松开后留 200 us 让 CPU 消费事件
end
endtask
task click; // 左键点一下
begin
press(0, 50);
end
endtask
// ---------------------------------------------------------------- 显存 dump
integer fh, w, b;
integer cursor_row, cursor_col;
// ---- 统计:MMIO 显存写脉冲数 + 当前非零格数 ----
integer fb_we_cnt = 0;
always @(posedge dut.clk_cpu)
if (dut.fb_we)
fb_we_cnt = fb_we_cnt + 1;
// ---- 鼠标事件链路三个关键节点 ----
// ptr_ev : cpu21_pointer 产生事件(ev_valid_o 高的周期数)
// irq_hi : 鼠标中断线 irq_vec[0] 为高的周期数
// irq_rise: 该线的上升沿次数(CPU 按上升沿响应中断)
integer ptr_ev = 0, irq_hi = 0, irq_rise = 0;
reg irq_prev_q;
initial
irq_prev_q = 1'b0;
always @(posedge dut.clk_cpu) begin
if (dut.g_btn.u_ptr.ev_valid_o)
ptr_ev = ptr_ev + 1;
if (dut.irq_vec[0])
irq_hi = irq_hi + 1;
irq_prev_q <= dut.irq_vec[0];
if (dut.irq_vec[0] && !irq_prev_q)
irq_rise = irq_rise + 1;
end
// 每毫秒打一行,观察事件链路
integer tick_ms;
initial
for (tick_ms = 1; tick_ms <= 20; tick_ms = tick_ms + 1) begin
#1000000;
$display("[mouse] %0t ns: ptr_ev=%0d irq_hi=%0d irq_rise=%0d", $time, ptr_ev, irq_hi, irq_rise);
end
integer nz_cnt = 0; // 当前非零格数(dump 时统计)
// ---- PC 热点直方图:确定 CPU 此刻卡在哪个循环 ----
integer pc_hist[0:255];
integer pi;
initial
for (pi = 0; pi < 256; pi = pi + 1)
pc_hist[pi] = 0;
always @(posedge dut.clk_cpu)
pc_hist[dut.u_cpu.pc_q[9:2]] = pc_hist[dut.u_cpu.pc_q[9:2]] + 1;
// ---- 探针:控制流日志 ----
// 1) 记录所有非顺序跳转(1 ms 之后、最多 300 条),用来复盘"跑飞"的路径;
// 2) 单独标出跳进 ISR 段之间空洞(0x194-0x1FF / 0x218-0x2FF)的时刻。
reg [31:0] pc_prev;
integer jmp_log;
initial begin
pc_prev = 32'hFFFF_FFFF;
jmp_log = 0;
end
always @(posedge dut.clk_cpu) begin
pc_prev <= dut.u_cpu.pc_q;
if (dut.u_cpu.pc_q != (pc_prev + 32'd4)) begin
if ($time > 1000000 && jmp_log < 300) begin
$display("[jmp] %0t ns: 0x%03h -> 0x%03h", $time, pc_prev, dut.u_cpu.pc_q);
jmp_log = jmp_log + 1;
end
if ((dut.u_cpu.pc_q >= 32'h0000_0194 && dut.u_cpu.pc_q < 32'h0000_0200) ||
(dut.u_cpu.pc_q >= 32'h0000_0218 && dut.u_cpu.pc_q < 32'h0000_0300))
$display("[gap] %0t ns: PC 0x%03h -> 0x%03h (跳进空洞)", $time, pc_prev, dut.u_cpu.pc_q);
end
end
task dump_pc;
integer i1, i2, best, besti;
begin
$display(" [pc] 热点(pc, 命中数):");
for (i1 = 0; i1 < 3; i1 = i1 + 1) begin
best = -1;
besti = -1;
for (i2 = 0; i2 < 256; i2 = i2 + 1)
if (pc_hist[i2] > best) begin
best = pc_hist[i2];
besti = i2;
end
if (besti >= 0 && best > 0) begin
$display(" pc=0x%03h count=%0d", besti << 2, best);
pc_hist[besti] = 0;
end
end
end
endtask
// ---- RAM 窗口(核对变量地址映射)----
task dump_ramwin;
integer i3;
begin
$display(" [ram] 0x00..0x3F: %08h %08h %08h %08h %08h %08h %08h %08h",
dut.ram[0], dut.ram[1], dut.ram[2], dut.ram[3],
dut.ram[4], dut.ram[5], dut.ram[6], dut.ram[7]);
$display(" [ram] 0x100..0x11F: %08h %08h %08h %08h %08h %08h %08h %08h",
dut.ram[32'h0100 >> 2], dut.ram[32'h0104 >> 2],
dut.ram[32'h0108 >> 2], dut.ram[32'h010C >> 2],
dut.ram[32'h0110 >> 2], dut.ram[32'h0114 >> 2],
dut.ram[32'h0118 >> 2], dut.ram[32'h011C >> 2]);
end
endtask
// 最新版(显存每格 2 字节、难度三档不同尺寸)变量地址:
// g_exit=0x04 g_in_level=0x08 acc_y=0x78C acc_x=0x790 cur_c=0x794
// cur_r=0x798 g_difficulty=0x7A0 g_mines=0x7A4 g_first_done=0x7A8
// g_state=0x7AC flags_cnt=0x7B0 opened_cnt=0x7B4
// flag_bm=0x7F4 open_bm=0x830 mine_bm=0x86C
// g_ncells=0x8B4 g_evq_lost=0x8C0 g_evq_tail=0x8C4 g_evq_head=0x8C8
task dump_vars;
begin
$display(" [vars] exit=%0d in_level=%0d cur=(r%0d,c%0d) acc=(%0d,%0d) diff=%0d mines=%0d first=%0d state=%0d flags=%0d opened=%0d lost=%0d evq(h=%0d,t=%0d)",
dut.ram[32'h0004 >> 2], dut.ram[32'h0008 >> 2],
dut.ram[32'h0798 >> 2], dut.ram[32'h0794 >> 2],
dut.ram[32'h0790 >> 2], dut.ram[32'h078C >> 2],
dut.ram[32'h07A0 >> 2], dut.ram[32'h07A4 >> 2],
dut.ram[32'h07A8 >> 2], dut.ram[32'h07AC >> 2],
dut.ram[32'h07B0 >> 2], dut.ram[32'h07B4 >> 2],
dut.ram[32'h08C0 >> 2],
dut.ram[32'h08C8 >> 2], dut.ram[32'h08C4 >> 2]);
end
endtask
task dump_fb;
input [1023:0] fname;
integer k2, b2;
begin
nz_cnt = 0;
for (k2 = 0; k2 < 600; k2 = k2 + 1)
for (b2 = 0; b2 < 4; b2 = b2 + 1)
if (dut.fb[k2][b2*8 +: 8] != 8'h00)
nz_cnt = nz_cnt + 1;
cursor_row = dut.u_vga.cursor_i[12:8];
cursor_col = dut.u_vga.cursor_i[4:0];
fh = $fopen(fname, "w");
$fwrite(fh, "# tb_saolei framebuffer dump\n");
$fwrite(fh, "TIME %0t\n", $time);
$fwrite(fh, "CURSOR %0d %0d\n", cursor_row, cursor_col);
$fwrite(fh, "CTRL 0x%02X\n", dut.u_vga.ctrl_i[7:0]);
$fwrite(fh, "STATS fb_we=%0d nonzero=%0d\n", fb_we_cnt, nz_cnt);
$fwrite(fh, "FB 2400\n");
for (w = 0; w < 600; w = w + 1)
for (b = 0; b < 4; b = b + 1)
$fwrite(fh, "%02X\n", dut.fb[w][b*8 +: 8]);
$fclose(fh);
$display("[tb_saolei] %0t ns dump -> %0s (cursor=%0d,%0d fb_we=%0d nonzero=%0d)",
$time, fname, cursor_row, cursor_col, fb_we_cnt, nz_cnt);
dump_vars;
dump_pc;
dump_ramwin;
end
endtask
// ---------------------------------------------------------------- 完整一局用的工具
// 从 RAM 的位图里取一位(mine_bm @0x86C / open_bm @0x830 / flag_bm @0x7F4)
function integer bitof;
input integer base;
input integer idx;
begin
bitof = (dut.ram[(base >> 2) + (idx >> 5)] >> (idx & 31)) & 1;
end
endfunction
// 已揭开格数:直接读程序里的 opened_cnt(0x7B4)
integer open_n;
task count_open;
begin
open_n = dut.ram[32'h07B4 >> 2];
end
endtask
// 快速按键(按住/松开时长都可控):去抖周期已由 POINTER_SAMPLE_DIV=50 缩短
task press_t;
input integer which;
input integer hold_us;
input integer gap_us;
begin
case (which)
0:
go = 1'b1;
1:
btnu = 1'b1;
2:
btnd = 1'b1;
3:
btnl = 1'b1;
default:
btnr = 1'b1;
endcase
#(hold_us * 1000);
case (which)
0:
go = 1'b0;
1:
btnu = 1'b0;
2:
btnd = 1'b0;
3:
btnl = 1'b0;
default:
btnr = 1'b0;
endcase
#(gap_us * 1000);
end
endtask
task click_t;
begin
press_t(0, 30, 150);
end
endtask
// 把光标移到盘面格 (tr, tc):每次按键后等软件把事件消化掉再回读位置,
// 丢了/多了事件都能自动纠正。盘面格 (0,0) 对应网格 (BOARD_R0, BOARD_C0) = (7, 12)。
task move_to_cell;
input integer tr;
input integer tc;
integer cr, cc2;
begin
cr = dut.ram[32'h0418 >> 2];
cc2 = dut.ram[32'h0414 >> 2];
while (cr > (7 + tr)) begin
press_t(1, 20, 120);
cr = dut.ram[32'h0418 >> 2];
end
while (cr < (7 + tr)) begin
press_t(2, 20, 120);
cr = dut.ram[32'h0418 >> 2];
end
while (cc2 > (12 + tc)) begin
press_t(3, 20, 120);
cc2 = dut.ram[32'h0414 >> 2];
end
while (cc2 < (12 + tc)) begin
press_t(4, 20, 120);
cc2 = dut.ram[32'h0414 >> 2];
end
end
endtask
// 落位 + 校验:没到位就重试(最多 6 次),彻底避免“以为到了、实际差一格”去点错格
task goto_cell;
input integer tr;
input integer tc;
integer tries;
begin
tries = 0;
while (((dut.ram[32'h0418 >> 2] != (7 + tr)) || (dut.ram[32'h0414 >> 2] != (12 + tc))) && (tries < 6)) begin
move_to_cell(tr, tc);
#(300 * 1000);
tries = tries + 1;
end
if (tries >= 6)
$display(" [game] !!! 定位 (%0d,%0d) 失败,实际 cur=(%0d,%0d)", tr, tc,
dut.ram[32'h0418 >> 2], dut.ram[32'h0414 >> 2]);
end
endtask
// 扫完全盘:安全格揭开、雷格插旗;雷区位置直接从 RAM 里的 mine_bm 读
task sweep_board;
integer rr, cc, idx;
begin : sweep_blk
for (rr = 0; rr < 16; rr = rr + 1) begin
for (cc = 0; cc < 16; cc = cc + 1) begin
idx = rr * 16 + cc;
goto_cell(rr, cc);
if (bitof(32'h0000_086C, idx) != 0) begin
sw_flag = 1'b1;
#(30 * 1000);
click_t;
sw_flag = 1'b0;
#(30 * 1000);
end
else
click_t;
// 中途退出关卡就立即报警并收手
if (dut.ram[32'h0008 >> 2] == 0) begin
$display(" [game] *** 在盘面 (%0d,%0d) 处退出了关卡!cur=(%0d,%0d) state=%0d",
rr, cc, dut.ram[32'h0798 >> 2], dut.ram[32'h0794 >> 2],
dut.ram[32'h07AC >> 2]);
disable sweep_blk;
end
// 赢了/输了也收手
if (dut.ram[32'h07AC >> 2] != 0)
disable sweep_blk;
end
count_open;
$display(" [game] 第 %0d 行扫完:已揭开 %0d / %0d, state=%0d, 旗=%0d, cur=(%0d,%0d)",
rr, open_n,
(dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
dut.ram[32'h07AC >> 2], dut.ram[32'h07B0 >> 2],
dut.ram[32'h0798 >> 2], dut.ram[32'h0794 >> 2]);
if (rr == 3)
dump_fb("fb_10_sweep_a.txt");
if (rr == 7)
dump_fb("fb_11_sweep_b.txt");
if (rr == 11)
dump_fb("fb_12_sweep_c.txt");
end
end
endtask
// ---------------------------------------------------------------- 主流程
initial begin
$display("[tb_saolei] === start (SAOLEI_ROM):完整一局 ===");
#2000;
rst_n = 1'b1;
// 1) 开始界面
#(2 * 1000000);
dump_fb("fb_00_start.txt");
// 2) 点 START(光标初始就停在按钮上)-> 写 LEVEL -> IRQ1 进关卡
click_t;
#(2 * 1000000);
dump_fb("fb_01_level.txt");
$display(" [game] 进关卡:in_level=%0d state=%0d", dut.ram[32'h0008 >> 2], dut.ram[32'h07AC >> 2]);
// 3) 关卡内点盘面中心一格:放雷 + 首次展开(第一下必安全)
click;
#(3 * 1000000);
dump_fb("fb_02_firstclick.txt");
count_open;
$display(" [game] 首次点击后:first=%0d 已揭开 %0d / %0d state=%0d diff=%0d",
dut.ram[32'h07A8 >> 2], open_n,
(dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
dut.ram[32'h07AC >> 2], dut.ram[32'h07A0 >> 2]);
// 4) 蛇形扫完全盘:揭开全部 216 个安全格 + 给 40 颗雷插旗 -> 目标通关
sweep_board;
#(3 * 1000000);
dump_fb("fb_13_after_sweep.txt");
count_open;
$display(" [game] 扫完后:已揭开 %0d / %0d, 旗=%0d, state=%0d, 丢事件=%0d",
open_n, (dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
dut.ram[32'h07B0 >> 2], dut.ram[32'h07AC >> 2], dut.ram[32'h08C0 >> 2]);
// 5) 通关后再点一下 = 返回背景(回到开始界面)
click_t;
#(3 * 1000000);
dump_fb("fb_14_back_to_start.txt");
$display(" [game] === 结束:state=%0d(1=WIN 2=LOSE)已揭开 %0d/%0d 旗=%0d 丢事件=%0d ===",
dut.ram[32'h07AC >> 2], open_n,
(dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
dut.ram[32'h07B0 >> 2], dut.ram[32'h08C0 >> 2]);
$finish;
end
endmodule
+201
View File
@@ -0,0 +1,201 @@
`timescale 1ns / 1ps
// =============================================================================
// tb_saolei_play —— 没有 VGA 显示器时"在命令行里玩扫雷"用的测试平台
//
// 与 tb_saolei 的区别:没有写死的游戏脚本,改成受外部文件驱动:
//
// cmd.txt 外部(xsim 的 Tcl,见 pipelined-cpu/tools/play_saolei.tcl)往里写
// 按键字符;本 TB 每 100 us 仿真时间轮询一次,读到**一个**字符就执行
// 对应的按键脉冲,然后把文件清空。(多个动作用多次发送实现,
// play_saolei.tcl 的 k 命令会自动把 "uuc" 拆成三次发。)
// u/w 光标上移 d/s 光标下移
// l/a 光标左移 r 光标右移
// c 左键点击(BTNC)
// f 插旗模式点击(先按 SW3 再点击,点上/取消旗帜)
// p 只切换 SW3(插旗模式开关,不点击)
// g 按板上的 go 键
// z 复位(拉低 rst_n 再放开,等于重新上电)
//
// now.txt 每 1 ms 仿真时间把当前显存 dump 一次(格式同 tb_saolei):
// py saolei/tools/fb2png.py --term now.txt -> 终端里的彩色棋盘
// py saolei/tools/fb2png.py now.txt a.png -> 640x480 截图
//
// 编译(两个 -d 都要带 SAOLEI_ROM):
// xvlog -d SAOLEI_ROM -i srcs/board -i srcs/rtl srcs/rtl/*.v \
// srcs/board/cpu21_board_top.v srcs/tb/tb_saolei_play.v
// xelab -d SAOLEI_ROM -top tb_saolei_play -snapshot saolei_play
// xsim saolei_play -tclbatch tools/play_saolei.tcl
//
// 说明:和 tb_saolei 一样用 POINTER_SAMPLE_DIV=50 压缩按键去抖(上板仍是 50000)。
// =============================================================================
module tb_saolei_play;
// ---- 板级顶层端口(全部由本 TB / cmd.txt 驱动,不用 add_force)----
reg clk100 = 1'b0;
reg rst_n = 1'b0;
reg go = 1'b0;
reg [ 2:0] irq_sw = 3'b000;
reg btnu = 1'b0;
reg btnd = 1'b0;
reg btnl = 1'b0;
reg btnr = 1'b0;
reg sw_flag = 1'b0;
wire [15:0] led;
wire led_we;
wire [ 7:0] an;
wire [ 6:0] seg;
wire dp;
wire [ 3:0] vga_r, vga_g, vga_b;
wire vga_hs, vga_vs;
always #5 clk100 = ~clk100;
cpu21_board_top #(
.POINTER_SAMPLE_DIV(50) // 仿真里加速按键去抖(上板默认 50000)
) dut (
.clk100 (clk100),
.rst_n (rst_n),
.go (go),
.irq_sw (irq_sw),
.btnu (btnu),
.btnd (btnd),
.btnl (btnl),
.btnr (btnr),
.sw_flag (sw_flag),
.led (led),
.led_we (led_we),
.an (an),
.seg (seg),
.dp (dp),
.vga_r (vga_r),
.vga_g (vga_g),
.vga_b (vga_b),
.vga_hs (vga_hs),
.vga_vs (vga_vs),
.ps2_clk (1'b1),
.ps2_data(1'b1)
);
// ---- 一个按键脉冲:按住 hold_us 再松开,然后留 200 us 让 CPU 消化事件 ----
task pulse;
input integer which;
input integer hold_us;
begin
case (which)
1:
btnu = 1'b1;
2:
btnd = 1'b1;
3:
btnl = 1'b1;
4:
btnr = 1'b1;
default:
go = 1'b1;
endcase
#(hold_us * 1000);
case (which)
1:
btnu = 1'b0;
2:
btnd = 1'b0;
3:
btnl = 1'b0;
4:
btnr = 1'b0;
default:
go = 1'b0;
endcase
#(200 * 1000);
end
endtask
// ---- 把 cmd.txt 里的一个字符变成按键动作 ----
// 注意:一次只取一个字符($fgetc),不用 $fgets/字符串,
// 这样不会踩 "reg [8*N:1] 里第一个字符落在哪个字节" 的坑。
task do_key;
input [7:0] ch;
begin
case (ch)
8'h75, 8'h77:
pulse(1, 20); // u / w 上
8'h64, 8'h73:
pulse(2, 20); // d / s 下
8'h6C, 8'h61:
pulse(3, 20); // l / a 左
8'h72:
pulse(4, 20); // r 右
8'h63:
pulse(0, 30); // c 左键点击
8'h67:
pulse(0, 30); // g go 键(同一次点击)
8'h66: begin // f 插旗模式点击
sw_flag = 1'b1;
#(30 * 1000);
pulse(0, 30);
#(30 * 1000);
sw_flag = 1'b0;
#(30 * 1000);
end
8'h70: begin // p 只切换插旗开关
sw_flag = ~sw_flag;
#(50 * 1000);
end
8'h7A: begin // z 复位
rst_n = 1'b0;
#(2 * 1000);
rst_n = 1'b1;
#(500 * 1000);
end
default:
; // 其它字符忽略
endcase
end
endtask
integer cfp;
integer chc;
// ---- 命令轮询:每 100 us 看一次 cmd.txt(只读第一个字符)----
initial begin
#2000;
rst_n = 1'b1; // 上电复位释放
forever begin
#(100 * 1000);
cfp = $fopen("cmd.txt", "r");
if (cfp != 0) begin
chc = $fgetc(cfp);
$fclose(cfp);
cfp = $fopen("cmd.txt", "w"); // 处理掉就清空,避免重复执行
if (cfp != 0)
$fclose(cfp);
if ((chc > 0) && (chc != 8'h0A) && (chc != 8'h0D) && (chc != 8'h20))
do_key(chc[7:0]);
end
end
end
// ---- 画面 dump:每 1 ms 覆盖写一次 now.txt ----
integer df, w2, b2;
initial begin
forever begin
#(1 * 1000 * 1000);
df = $fopen("now.txt", "w");
if (df != 0) begin
$fwrite(df, "TIME %0t\n", $time);
$fwrite(df, "CURSOR %0d %0d\n",
dut.u_vga.cursor_i[12:8], dut.u_vga.cursor_i[4:0]);
$fwrite(df, "CTRL 0x%02X\n", dut.u_vga.ctrl_i[7:0]);
$fwrite(df, "FB 2400\n");
for (w2 = 0; w2 < 600; w2 = w2 + 1)
for (b2 = 0; b2 < 4; b2 = b2 + 1)
$fwrite(df, "%02X\n", dut.fb[w2][b2*8 +: 8]);
$fclose(df);
end
end
end
endmodule
+414
View File
@@ -0,0 +1,414 @@
#!/usr/bin/env python3
"""把中断测试程序适配到这块 CPU 上板运行(只改 ROM 内容,不改 CPU 语义)。
背景
----
programs/risc-v-intrupt-ccab.hex 是课程的中断/异常测试程序(270 字)。
直接放进板上 ROM 会有两个问题:
1) ISR 里的 ecall 会停机
三个中断服务程序里各有一条 "addi a7,zero,10" + "ecall"。这颗 CPU 的约定是
a7==34 的 ecall 是历史"显示"调用(无操作),其余 a7 值一律让流水线停机并等待
go_i。于是 ISR 跑到这里就卡住:后面的中断服务显示循环永远不执行,主程序的
跑马灯也停在原地。
-> 把 "addi a7,zero,10" + "ecall" 换成两条 NOP。
2) 显示刷新太快,数码管上看不出跑马灯
原来的显示是 "lui a7,1" + "sw a0,0(a7)"(写 MMIO 的 LED 寄存器)。主循环里
两次显示只隔几拍(50 MHz 下约 0.1 us),而数码管是按位动态扫描的,根本来不及
分辨,只会看到一片亮。
-> 把每条显示换成 "jal ra,DISP_DLY" + "nop",由追加在 ROM 末尾的 DISP_DLY
完成"写 LED 寄存器 + 约 10 ms 延时"。
(注意:早先把旧的 "addi a7,zero,34 + ecall" 换成 MMIO 存储时,rs1 域算错,
把 sw 写成了 sb —— 只更新 LED 寄存器的低字节,高位一直是旧值,显示自然是
错的。DISP_DLY 内部统一用正确的 sw 写整个字,这个脚本会顺带纠正。)
3) **修正 ISR2 / ISR3 的现场恢复顺序**
原始 hex 是手工改过的,两个 ISR 的出栈顺序被写乱(ISR1 是对的):
* ISR3:把 s1 从 s3 的槽位弹出,于是 s1 被写成 s3 保存的值 0xFFFFF111;
ISR3 返回后,正在跑 BLT 测试的 ISR2 的循环计数 s1 就变成了 -3823,
循环要跑 3800 多次(约 170 万周期)才退出 —— 看起来就是“二级中断返回
之后卡住”;
* ISR2:s0/s2/a0/a7/s1/t3/t1 全部错位,返回后调用者的这些寄存器被破坏。
-> 按 asm 清单里正确的 LIFO 顺序,只改 12 条 `lw` 的目标寄存器(字数不变、
地址不动),ISR3 改 5 条、ISR2 改 7 条。
5) **修正 ISR3 的 SB 测试用到的未保存寄存器**
ISR3 的现场只保存了 s1/a7/a0/s0/s3/s4/s5/s6/uepc,但它的 SB 测试却用
t1(地址)、t3(计数)、s2(增量)—— 这三个都没保存。后果:
* 若 IRQ3 抢占正在跑 DIVU 测试的 ISR1(ISR1 用 t3 做递减计数),ISR1 的
计数会被 ISR3 改成 0;ISR1 恢复后 `addi t3,t3,-1` 得到 -1,`bne t3,zero`
永远成立 —— ISR1 的 DIVU 循环再也出不来,表现为“三级中断之后卡住”;
* 主程序的跑马灯步长 t1 也会被改掉。
-> 改用 ISR3 自己保存的 s4/s5/s6:SB 测试结束后紧接着的 IntLoop3
(0x3B4 起)会重新给它们赋值,所以这样换是安全的;同样只改寄存器域,
字数与地址不变。
6) **ISR 不能破坏被打断代码的 ra**
三个 ISR 的现场里都保存了 s1/a7/a0/s0/s3/s4/s5/s6(外加各自的测试寄存器),
但**都没有保存 ra**,而它们要调用显示例程,`jal ra,DISP_DLY` 会把 ra 写成
ISR 内部的返回地址。后果:如果中断恰好落在被打断代码依赖 ra 的位置
—— 典型就是主程序正停在 DISP_DLY 末尾的 `jalr zero,0(ra)`(第 0x454 字)
—— 中断返回后 `jalr` 会跳到 ra 里的地址,也就是 ISR 内部的某条指令上,
于是一路执行到 ISR 结尾的 `csrrci zero,0x4,1`,把 MIE 永久清零:
“跑一阵子之后拨开关再也不响应,只能按复位”。窗口只有 1~2 拍,所以平时
很难遇到(仿真里把延时改到极小才会碰上)。
-> 给 ISR 另加一份显示例程 DISP_DLY_ISR(0x458),链接寄存器改用 **a0**,
显示值改走 **a7**:这两个寄存器三个 ISR 都保存/恢复,所以
* ISR 完全不碰 ra,被打断的代码停在 DISP_DLY 哪里都安全;
* ISR 自己被内层中断打断时,内层 ISR 返回也会把 a0/a7 恢复成被打断时
的值,所以 DISP_DLY_ISR 自己的 `jalr zero,0(a0)` 同样安全。
主程序的 3 个显示点仍用原来的 DISP_DLY(链接在 ra,行为不变)。
每个 ISR 显示点:`jal ra,DISP_DLY` + `nop` 换成
`add a7,a0,zero` + `jal a0,DISP_DLY_ISR` —— 仍是 2 字换 2 字。
以上替换都是 **2 字换 2 字**,两个例程又追加在最后一条指令(uret)之后,所以
**所有指令地址都保持不变**,三个中断入口仍然是 0x0AC / 0x1E4 / 0x308。
DISP_DLY(8 字,追加在程序末尾)
--------------------------------
sw ra, 256(sp) # 保存返回地址
lui a7, 1 # a7 = 0x1000 = LED_ADDR
sw a0, 0(a7) # 把 a0 写进 LED 寄存器(数码管显示它的 8 位十六进制)
lui t2, DELAY # 计数初值为负,向上数到 0(约 10 ms @50 MHz)
1: addi t2, t2, 1
blt t2, zero, 1b
lw ra, 256(sp)
jalr zero, ra, 0
为什么返回地址存在 sp+256 而不是程序栈里:
本程序的 ISR 是“先把 sp 加上去,再往里存”的向上增长栈,一层最多占 48 字节,
最多嵌套 4 层(IRQ_STACK_DEPTH=4)共 192 字节,所以 [sp, sp+192) 是中断随时
可能写到的区域。把返回地址放在 sp+256(越过了这个区域):
* 从 ISR 里调用时,不会覆盖 ISR 存好的 s6 / uepc(早期用 sp-8 就是这么坏掉的:
uepc 被写坏后 uret 会返回到 ISR 内部地址,最后执行到 csrrci 把 MIE 永久清零);
* 中断(包括多层嵌套)随时打断延时也不会破坏它,因为嵌套 ISR 只会在
[sp, sp+192) 里压栈;
* 每次调用各用自己 sp+256 的位置,所以嵌套调用也互不影响。
延时计数不用“递减到 0”而用“负数向上数到 0”:万一被嵌套调用改掉了 t2,循环也
只会在 t2 >= 0 时立即退出,不会像递减写法那样绕成 40 亿次而死等。
用法
----
python tools/adapt_intrupt_ccab.py <输入.hex> <输出.vh> [输出_fast.hex]
<输入.hex> programs/risc-v-intrupt-ccab.hex(未适配的原始镜像)
<输出.vh> srcs/board/risc-v-intrupt-ccab.vh(上板综合用)
[输出_fast.hex] 可选:延时改成约 0.6 us 的仿真专用版本。上板一次中断服务要跑
几十毫秒(几十万个时钟),仿真跑完整流程会很慢,因此仿真用这个
快版来验证控制流(入口 / 中断返回 / 不停机),延时量本身不影响
控制流。
"""
import sys
NOP = 0x00000013
LED_ADDR_HI = 1 # lui a7, 1 -> a7 = 0x1000(LED_ADDR)
# 延时计数:计数初值为负,向上数到 0。0xFFFC0000 -> 0x40000 = 262144 次
# (每轮约 2 个时钟)= 52.4 万个时钟 ≈ 10 ms @50 MHz。
DELAY_NEG_HI = 0xFFFC0
DELAY_FAST_NEG = -16 # 仿真用:addi t2, zero, -16 ≈ 0.6 us
# DISP_DLY 保存返回地址的偏移:必须大于 ISR 最大嵌套栈用量(4 层 x 48B = 192B)
LINK_SLOT = 256
# 寄存器号
ZERO, RA, SP, GP, T2, A0, A7 = 0, 1, 2, 3, 7, 10, 17
def enc_i(imm, rs1, f3, rd, op=0x13):
return ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op
def enc_s(imm, rs2, rs1, f3):
imm &= 0xFFF
return (((imm >> 5) << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12)
| ((imm & 0x1F) << 7) | 0x23)
def enc_b(imm, rs2, rs1, f3):
imm &= 0x1FFF
return ((((imm >> 12) & 1) << 31) | (((imm >> 5) & 0x3F) << 25)
| (rs2 << 20) | (rs1 << 15) | (f3 << 12)
| (((imm >> 1) & 0xF) << 8) | (((imm >> 11) & 1) << 7) | 0x63)
def enc_u(imm, rd):
return ((imm & 0xFFFFF) << 12) | (rd << 7) | 0x37
def enc_j(imm, rd):
imm &= 0x1FFFFF
return ((((imm >> 20) & 1) << 31) | (((imm >> 1) & 0x3FF) << 21)
| (((imm >> 11) & 1) << 20) | (((imm >> 12) & 0xFF) << 12)
| (rd << 7) | 0x6F)
HALT_LOAD = enc_i(10, ZERO, 0, A7) # addi a7, zero, 10
ECALL = 0x00000073
DISP_LUI = enc_u(LED_ADDR_HI, A7) # lui a7, 1
DISP_SW = enc_s(0, A0, A7, 2) # sw a0, 0(a7) <- 正确:字写
DISP_SB_BAD = enc_s(0, A0, A7, 0) # sb a0, 0(a7) <- 误写成字节写,一并修正
# 说明:这两种都当成"显示点"。DISP_DLY 内部一律用正确的 sw 写整个字,因此即使
# 输入镜像里是 sb(只写低字节,显示的高位会残留旧值),适配后也会恢复正常。
def disp_dly(delay_word):
"""DISP_DLY 的 8 条指令(delay_word 决定延时长短)。
返回地址存在 sp+LINK_SLOT,位于 ISR 可能压栈的区域之上,所以中断(含嵌套)
打断延时也不会破坏它 —— 见文件头的说明。
"""
return [
enc_s(LINK_SLOT, RA, SP, 2), # sw ra, 256(sp)
DISP_LUI, # lui a7, 1
DISP_SW, # sw a0, 0(a7)
delay_word, # lui t2, 0xFFFC0 / addi t2, zero, -16
enc_i(1, T2, 0, T2), # addi t2, t2, 1
enc_b(-4, ZERO, T2, 4), # blt t2, zero, -4
enc_i(LINK_SLOT, SP, 2, RA, 0x03), # lw ra, 256(sp)
enc_i(0, RA, 0, ZERO, 0x67), # jalr zero, ra, 0
]
def disp_dly_isr(delay_word):
"""ISR 专用的显示/延时例程(8 条)—— 链接寄存器用 a0,不用 ra。
三个 ISR 的现场都保存了 a7 与 a0(s1/a7/a0/s0/s3/s4/s5/s6),所以把链接放
a0、把显示值放 a7 不会额外破坏任何东西;而 ra 完全不被触碰,被打断的代码
即使停在主 DISP_DLY 的 `jalr zero,0(ra)` 上也不会被破坏。
"""
return [
enc_s(LINK_SLOT, A0, SP, 2), # sw a0, 256(sp) 保存链接寄存器
enc_u(LED_ADDR_HI, A0), # lui a0, 1 a0 = 0x1000
enc_s(0, A7, A0, 2), # sw a7, 0(a0) 显示 a7
delay_word, # lui t2, 0xFFFC0 / addi t2,zero,-16
enc_i(1, T2, 0, T2), # addi t2, t2, 1
enc_b(-4, ZERO, T2, 4), # blt t2, zero, -4
enc_i(LINK_SLOT, SP, 2, A0, 0x03), # lw a0, 256(sp)
enc_i(0, A0, 0, ZERO, 0x67), # jalr zero, a0, 0
]
# 三个中断入口的原指令(核对地址没有跑偏)
ISR_ENTRY = {43: 0x00512023, 121: 0x00612023, 194: 0x00912023}
# 三个 ISR 的代码从 ISR1 入口开始一直排到程序末尾(主程序在它之前)
ISR1_WORD = min(ISR_ENTRY)
# 需要修正目标寄存器的 `lw X,0(sp)`:addr -> (当前错误的 rd, 正确的 rd)
# ISR3(入口 0x308)恢复顺序应为:uepc,s6,s5,s4,s3,s0,a0,a7,s1
# ISR2(入口 0x1E4)恢复顺序应为:uepc,s6,s5,s4,s3,s0,a0,a7,s2,s1,t3,t1
FIX_POPS = {
0x410: (9, 19), 0x418: (19, 8), 0x420: (8, 10),
0x428: (10, 17), 0x430: (17, 9), # ISR3
0x2D0: (18, 8), 0x2D8: (9, 10), 0x2E0: (28, 17),
0x2E8: (6, 18), 0x2F0: (8, 9), 0x2F8: (10, 28),
0x300: (17, 6), # ISR2
}
# ISR3 的 SB 测试用了未保存的 t1/t3/s2 -> 改成 ISR3 自己保存的 s4/s5/s6
# addr -> (要改的域, 原寄存器号, 新寄存器号)
T1, T3, S2, S4, S5, S6 = 6, 28, 18, 20, 21, 22
FIX_ISR3_SB = {
0x358: ('rd', T1, S4), 0x35C: ('rd', T3, S5),
0x364: ('rd', S2, S6), 0x368: ('rs1', T1, S4),
0x378: ('rs2', S2, S6), 0x37C: ('rd_rs1', T1, S4),
0x380: ('rd_rs1', T3, S5), 0x384: ('rs1', T3, S5),
0x388: ('rd', T3, S5), 0x38C: ('rd', T1, S4),
0x390: ('rs1', T1, S4), 0x3A0: ('rd_rs1', T1, S4),
0x3A4: ('rd_rs1', T3, S5), 0x3A8: ('rs1', T3, S5),
}
def read_hex(path):
words = []
with open(path, encoding="ascii") as f:
for line in f:
line = line.split("//")[0].strip()
if line:
words.append(int(line, 16))
return words
def patch(words):
"""返回 (适配后的字表, 适配后的快版字表, 统计信息)。"""
halt_sites = [i for i in range(len(words) - 1)
if words[i] == HALT_LOAD and words[i + 1] == ECALL]
if len(halt_sites) != 3:
raise SystemExit("预期 3 处 'addi a7,zero,10 + ecall'(ISR 停机点),"
"实际找到 %d 处;输入文件不是预期的原始镜像?" % len(halt_sites))
disp_sites = [i for i in range(len(words) - 1)
if words[i] == DISP_LUI
and words[i + 1] in (DISP_SW, DISP_SB_BAD)]
if len(disp_sites) != 11:
raise SystemExit("预期 11 处 'lui a7,1 + sw/sb a0,0(a7)'(显示点),"
"实际找到 %d 处;输入文件不是预期的原始镜像?" % len(disp_sites))
bad_store = [i for i in disp_sites if words[i + 1] == DISP_SB_BAD]
for i in halt_sites:
words[i] = NOP
words[i + 1] = NOP
# plain 变体:只去掉停机 ecall,显示保持原来的 "lui + sw" 两字写法
# (必须在把显示点改成 jal 之前取副本,否则会生成“跳到没有函数体的地址”的镜像)
plain = list(words)
# DISP_DLY 紧跟在原程序的最后一条指令之后(原程序长度不变)
disp_pc = 4 * len(words)
isr_pc = disp_pc + 32 # ISR 专用的 DISP_DLY_ISR 再占 8 字
isr_sites = []
for i in disp_sites:
if i >= ISR1_WORD:
# 三个 ISR 里的显示点:改用 a0 做链接寄存器、显示值走 a7
# 注意 jal 落在显示点的后一条(i+1)上,偏移要相对它自己算
isr_sites.append(i)
words[i] = enc_i(0, A0, 0, A7) # add a7, a0, zero
words[i + 1] = enc_j(isr_pc - 4 * (i + 1), A0) # jal a0, DISP_DLY_ISR
else:
# 主程序里的显示点:照旧用 ra 链接
words[i] = enc_j(disp_pc - 4 * i, RA)
words[i + 1] = NOP
if not isr_sites:
raise SystemExit("没有找到 ISR 里的显示点;中断入口地址对不上?")
# 自检:把刚写进去的 jal 偏移反解出来,确认真的跳到两个例程的入口
def jal_target(w, pc):
imm = (((w >> 31) & 1) << 20) | (((w >> 21) & 0x3FF) << 1) \
| (((w >> 20) & 1) << 11) | (((w >> 12) & 0xFF) << 12)
if imm & 0x100000:
imm -= 0x200000
return pc + imm
for i in disp_sites:
if i >= ISR1_WORD:
want, at = isr_pc, 4 * (i + 1) # ISR:jal 在显示点后一条
else:
want, at = disp_pc, 4 * i # 主程序:jal 就在显示点上
got = jal_target(words[at // 4], at)
if got != want:
raise SystemExit("显示点 0x%03X 的跳转目标算错了:0x%03X != 0x%03X"
% (4 * i, got, want))
for idx, want in ISR_ENTRY.items():
if words[idx] != want:
raise SystemExit("中断入口 0x%03X 处的指令被改动了(%08X != %08X)"
% (4 * idx, words[idx], want))
# --- 4) 修正 ISR2 / ISR3 的现场恢复顺序(只改 lw 的 rd 域) ---
fixed_pops = 0
for pc, (bad_rd, rd) in FIX_POPS.items():
w = words[pc // 4]
op, f3 = w & 0x7F, (w >> 12) & 7
rs1, imm = (w >> 15) & 0x1F, (w >> 20) & 0xFFF
old_rd = (w >> 7) & 0x1F
if not (op == 0x03 and f3 == 2 and rs1 == SP and imm == 0):
raise SystemExit("0x%03X 不是预期的 `lw X,0(sp)`(%08X)" % (pc, w))
if old_rd == rd:
continue # 已经是对的(重复运行时跳过)
if old_rd != bad_rd:
raise SystemExit("0x%03X 的目标寄存器不是预期的 %d(实际 %d)"
% (pc, bad_rd, old_rd))
words[pc // 4] = (w & ~(0x1F << 7)) | (rd << 7)
fixed_pops += 1
# --- 5) 把 ISR3 SB 测试里的 t1/t3/s2 换成 s4/s5/s6 ---
def set_field(w, name, val):
if name == 'rd':
return (w & ~(0x1F << 7)) | (val << 7)
if name == 'rs1':
return (w & ~(0x1F << 15)) | (val << 15)
if name == 'rs2':
return (w & ~(0x1F << 20)) | (val << 20)
if name == 'rd_rs1':
return ((w & ~(0x1F << 7)) | (val << 7)) & ~(0x1F << 15) | (val << 15)
raise SystemExit('未知域 ' + name)
fixed_sb = 0
for pc, (field, old, new) in FIX_ISR3_SB.items():
w = words[pc // 4]
idx = {'rd': (w >> 7) & 0x1F, 'rs1': (w >> 15) & 0x1F,
'rs2': (w >> 20) & 0x1F}
if field == 'rd_rs1':
cur = idx['rd']
if idx['rs1'] != cur:
raise SystemExit('0x%03X 的 rd/rs1 不一致' % pc)
else:
cur = idx[field]
if cur == new:
continue # 已经改过了
if cur != old:
raise SystemExit('0x%03X 的 %s 不是预期的 %d(实际 %d)'
% (pc, field, old, cur))
words[pc // 4] = set_field(w, field, new)
fixed_sb += 1
slow = disp_dly(enc_u(DELAY_NEG_HI, T2)) + disp_dly_isr(enc_u(DELAY_NEG_HI, T2))
fast = disp_dly(enc_i(DELAY_FAST_NEG, ZERO, 0, T2)) \
+ disp_dly_isr(enc_i(DELAY_FAST_NEG, ZERO, 0, T2))
return (words + slow, words + fast, plain,
(halt_sites, disp_sites, disp_pc,
[i for i in disp_sites if i < ISR1_WORD], isr_sites,
bad_store, fixed_pops, fixed_sb))
def write_hex(path, words):
with open(path, "w", encoding="ascii", newline="\n") as f:
for w in words:
f.write("%08X\n" % w)
def write_vh(path, words, src_name, total, note):
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(" // 由 tools/adapt_intrupt_ccab.py 生成 —— 不要手改\n")
f.write(" // source: %s(%d 字,其余地址填 NOP)\n"
% (src_name, len(words)))
f.write(" // %s\n" % note)
for addr in range(total):
w = words[addr] if addr < len(words) else NOP
f.write(" rom[%d] = 32'h%08X;\n" % (addr, w))
def main():
if len(sys.argv) < 3:
raise SystemExit(__doc__.strip().split("用法")[-1])
src, dst_vh = sys.argv[1], sys.argv[2]
dst_fast = sys.argv[3] if len(sys.argv) > 3 else None
dst_plain = sys.argv[4] if len(sys.argv) > 4 else None
words = read_hex(src)
slow, fast, plain, (halt_sites, disp_sites, disp_pc, main_sites, isr_sites,
bad_store, fixed_pops, fixed_sb) = patch(words)
total = 1024
write_vh(dst_vh, slow, src, total,
"已适配:去掉 ISR 停机 ecall、显示改为 MMIO+DISP_DLY(约 10 ms)")
if dst_fast:
write_hex(dst_fast, fast)
if dst_plain:
write_hex(dst_plain, plain)
print("适配完成:%s(原程序 %d 字 + DISP_DLY %d 字 = %d 字)"
% (dst_vh, len(words), len(slow) - len(words), len(slow)))
print(" 去掉的停机 ecall 位置:%s"
% ", ".join("0x%03X" % (4 * i) for i in halt_sites))
print(" 换成显示调用的点:主程序 %s|ISR %s"
% (", ".join("0x%03X" % (4 * i) for i in main_sites),
", ".join("0x%03X" % (4 * i) for i in isr_sites)))
print(" DISP_DLY 本体:0x%03X - 0x%03X(主程序用,链接在 ra)"
% (disp_pc, disp_pc + 28))
print(" DISP_DLY_ISR 本体:0x%03X - 0x%03X(ISR 用,链接在 a0,不碰 ra)"
% (disp_pc + 32, disp_pc + 60))
print(" 修正的现场恢复顺序:%d 条" % fixed_pops)
print(" ISR3 SB 测试改用的已保存寄存器:%d 条" % fixed_sb)
if bad_store:
print(" 顺带修正的字节写显示:%s"
% ", ".join("0x%03X" % (4 * i) for i in bad_store))
print(" 中断入口(不变):0x0AC / 0x1E4 / 0x308")
if dst_fast:
print(" 仿真快版:%s(延时约 0.6 us)" % dst_fast)
main()
+222
View File
@@ -0,0 +1,222 @@
#!/usr/bin/env python3
"""把课程新版中断测试(EPC 内存堆栈保护 + 附加 SLL/SB/BLTU)适配到本 CPU 上板运行。
输入:programs/risc-v-intrupt-sllsb.hex
课程的原始镜像(344 字,已去掉 Logisim 的 "v2.0 raw" 头)
输出:<输出.vh> 供 cpu21_board_top.v 里 `include 的 ROM 初始化(RISC_V_INTRUPT_SLLSB_ROM)
[输出_adapted.hex] 可选:适配后的镜像本身(方便人工对比/仿真)
只改 ROM 内容,不改 CPU 语义,**所有指令地址保持不变**,因此顶层的
IRQ1/2/3_VECTOR = 0x0AC / 0x240 / 0x3E8 不用动。
改了什么
--------
课程程序用 "addi a7,zero,34" + "ecall" 显示(`ecall` 的服务号 34 是原 Logisim
电路的历史"显示"调用;本 CPU 把它保留为空操作,LED/数码管不会有任何反应)。
这里把每条显示点**原地换成一次调用**(仍然是 2 字换 2 字):
* 主程序(字 0..42): "jal ra,DISP_DLY" + "nop"
* 三个 ISR(字 >= 43): "add a7,a0,zero" + "jal a0,DISP_DLY_ISR"
两个显示例程(各 8 条)追加在程序末尾(最后一条 uret 之后),所以入口地址不变。
为什么 ISR 版不用 ra
--------------------
三个 ISR 的现场都保存/恢复 a7 与 a0,但**都没有保存 ra**(它们原来的显示是
ecall,不需要 ra)。若 ISR 里用 `jal ra,...`,ra 会被写成 ISR 内部地址;恰好
中断落在主程序 DISP_DLY 末尾的 `jalr zero,0(ra)` 上时,返回后会跳到 ISR 内部,
一路执行到 ISR 结尾的 `csrrci zero,0x4,1`,把 MIE 永久清零(表现就是"跑一阵子
之后拨开关再也不响应")。所以 ISR 版把链接寄存器放 a0、显示值放 a7。
为什么返回地址存在 sp+256
------------------------
本程序的 ISR 是"先加 sp 再存"的向上增长栈,一层最多 48 字节,最多嵌套 4 层共
192 字节,所以 [sp, sp+192) 是中断随时会写到的区域。返回地址放在 sp+256(越过
这个区域)后,中断(含嵌套)打断显示延时也破坏不了它。
用法
----
py tools/adapt_intrupt_sllsb.py programs/risc-v-intrupt-sllsb.hex \
srcs/board/risc-v-intrupt-sllsb.vh [programs/risc-v-intrupt-sllsb-adapted.hex]
"""
import sys
NOP = 0x00000013
ECALL = 0x00000073
ZERO, RA, SP, T2, A0, A7 = 0, 1, 2, 7, 10, 17
LED_ADDR_HI = 1 # lui a7, 1 -> a7 = 0x1000(LED_ADDR / 数码管)
# 延时计数初值为负、向上数到 0(约 2 拍一轮):
# 0xFFFC0000 -> 0x40000 = 262144 轮 ≈ 52 万拍 ≈ 10 ms @50 MHz
# @3.125 MHz(CLK_DIV_INTRUPT=32)时约 160 ms,肉眼看得很清楚
DELAY_NEG_HI = 0xFFFC0
LINK_SLOT = 256 # 链接寄存器保存位置,必须 > ISR 最大栈用量(4x48=192)
ISR1_WORD = 43 # 字 43 起是三个 ISR(主程序在它之前)
ROM_WORDS = 1024 # 与 cpu21_board_top 的 hex2vh 约定一致(ROM 深度 8192 也够)
# ---------------------------------------------------------------- 编码
def enc_i(imm, rs1, f3, rd, op=0x13):
return ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op
def enc_s(imm, rs2, rs1, f3):
imm &= 0xFFF
return (((imm >> 5) << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12)
| ((imm & 0x1F) << 7) | 0x23)
def enc_b(imm, rs2, rs1, f3):
imm &= 0x1FFF
return ((((imm >> 12) & 1) << 31) | (((imm >> 5) & 0x3F) << 25)
| (rs2 << 20) | (rs1 << 15) | (f3 << 12)
| (((imm >> 1) & 0xF) << 8) | (((imm >> 11) & 1) << 7) | 0x63)
def enc_u(imm, rd):
return ((imm & 0xFFFFF) << 12) | (rd << 7) | 0x37
def enc_j(imm, rd):
imm &= 0x1FFFFF
return ((((imm >> 20) & 1) << 31) | (((imm >> 1) & 0x3FF) << 21)
| (((imm >> 11) & 1) << 20) | (((imm >> 12) & 0xFF) << 12)
| (rd << 7) | 0x6F)
def enc_r(f7, rs2, rs1, f3, rd):
return ((f7 << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12)
| (rd << 7) | 0x33)
DISP_LUI = enc_u(LED_ADDR_HI, A7) # lui a7, 1
DISP_SW = enc_s(0, A0, A7, 2) # sw a0, 0(a7)
SHOW_SITE = enc_i(34, ZERO, 0, A7) # addi a7, zero, 34 <- 课程程序的"显示"点
SW_A7_0SP = enc_s(0, A7, SP, 2) # sw a7, 0(sp) <- ISR 入口第一条
def disp_dly():
"""主程序用:显示 a0,延时约 10 ms,链接在 ra(8 条)。"""
return [
enc_s(LINK_SLOT, RA, SP, 2), # sw ra, 256(sp)
DISP_LUI, # lui a7, 1
DISP_SW, # sw a0, 0(a7)
enc_u(DELAY_NEG_HI, T2), # lui t2, 0xFFFC0
enc_i(1, T2, 0, T2), # addi t2, t2, 1
enc_b(-4, ZERO, T2, 4), # blt t2, zero, -4
enc_i(LINK_SLOT, SP, 2, RA, 0x03), # lw ra, 256(sp)
enc_i(0, RA, 0, ZERO, 0x67), # jalr zero, ra, 0
]
def disp_dly_isr():
"""ISR 用:显示 a7,链接在 a0,完全不碰 ra(8 条)。"""
return [
enc_s(LINK_SLOT, A0, SP, 2), # sw a0, 256(sp)
enc_u(LED_ADDR_HI, A0), # lui a0, 1
enc_s(0, A7, A0, 2), # sw a7, 0(a0)
enc_u(DELAY_NEG_HI, T2), # lui t2, 0xFFFC0
enc_i(1, T2, 0, T2), # addi t2, t2, 1
enc_b(-4, ZERO, T2, 4), # blt t2, zero, -4
enc_i(LINK_SLOT, SP, 2, A0, 0x03), # lw a0, 256(sp)
enc_i(0, A0, 0, ZERO, 0x67), # jalr zero, a0, 0
]
def read_hex(path):
words = []
with open(path, encoding="ascii") as f:
for line in f:
line = line.split("//")[0].strip()
if not line or line.lower().startswith("v2.0"):
continue # 容忍 Logisim 的 "v2.0 raw" 头
words.append(int(line, 16))
return words
def main():
if len(sys.argv) < 3:
raise SystemExit(__doc__)
src, dst_vh = sys.argv[1], sys.argv[2]
dst_hex = sys.argv[3] if len(sys.argv) > 3 else None
w = read_hex(src)
print("源程序 %d 字" % len(w))
# ---- 自检:三个 ISR 入口必须还是 sw a7,0(sp) ----
entries = [i for i, x in enumerate(w) if x == SW_A7_0SP]
print("ISR 入口候选(sw a7,0(sp))字号 = %s" % entries)
if entries != [43, 144, 250]:
print("警告:ISR 入口与预期 [43, 144, 250] 不一致,请核对顶层 IRQ_VECTOR!")
# ---- 例程地址(追加在程序末尾,入口地址因此不变)----
n = len(w)
dly_addr = n * 4
dly_isr_addr = (n + 8) * 4
# ---- 逐条替换显示点 ----
patched_main = patched_isr = 0
i = 0
while i < n - 1:
if w[i] == SHOW_SITE and w[i + 1] == ECALL:
if i < ISR1_WORD:
w[i] = enc_j(dly_addr - i * 4, RA) # jal ra, DISP_DLY
w[i + 1] = NOP
patched_main += 1
else:
w[i] = enc_r(0, 0, A0, 0, A7) # add a7, a0, zero
# 注意:jal 落在 w[i+1],所以偏移必须按 (i+1) 算,否则会跳过例程首条
w[i + 1] = enc_j(dly_isr_addr - (i + 1) * 4, A0) # jal a0, DISP_DLY_ISR
patched_isr += 1
i += 2
else:
i += 1
print("显示点替换:主程序 %d 处、ISR %d 处" % (patched_main, patched_isr))
# ---- 自检:每个显示调用的跳转目标必须正好落在例程首字 ----
def jal_target(word, addr):
imm = (((word >> 31) & 1) << 20) | (((word >> 21) & 0x3FF) << 1) \
| (((word >> 20) & 1) << 11) | (((word >> 12) & 0xFF) << 12)
if imm & (1 << 20):
imm -= (1 << 21)
return addr + imm
bad = []
for i in range(n - 1):
if w[i] == enc_r(0, 0, A0, 0, A7) and (w[i + 1] & 0x7F) == 0x6F:
if jal_target(w[i + 1], (i + 1) * 4) != dly_isr_addr:
bad.append(i)
if (w[i] & 0x7F) == 0x6F and ((w[i] >> 7) & 0x1F) == RA and w[i + 1] == NOP:
if jal_target(w[i], i * 4) != dly_addr:
bad.append(i)
if bad:
raise SystemExit("自检失败:字 %s 的跳转目标不对" % bad)
print("自检通过:所有显示调用都精确跳到例程首字(DISP_DLY 0x%X / ISR 0x%X)"
% (dly_addr, dly_isr_addr))
w += disp_dly() + disp_dly_isr()
print("追加 DISP_DLY @字 %d (0x%X)、DISP_DLY_ISR @字 %d (0x%X)"
% (n, dly_addr, n + 8, dly_isr_addr))
# ---- 写 .vh(与 tools/hex2vh.py 的格式一致)----
if len(w) > ROM_WORDS:
raise SystemExit("程序 %d 字,超过 %d 字上限" % (len(w), ROM_WORDS))
with open(dst_vh, "w", encoding="utf-8") as f:
f.write(" // generated by tools/adapt_intrupt_sllsb.py - do not edit by hand\n")
f.write(" // source: %s (%d words, remaining entries filled with NOP)\n"
% (src, len(w)))
for a in range(ROM_WORDS):
f.write(" rom[%d] = 32'h%08X;\n" % (a, w[a] if a < len(w) else NOP))
print("已生成 %s(%d 个字)" % (dst_vh, ROM_WORDS))
if dst_hex:
with open(dst_hex, "w", encoding="ascii") as f:
for x in w:
f.write("%08X\n" % x)
print("已生成 %s(适配后的镜像)" % dst_hex)
main()
+101
View File
@@ -0,0 +1,101 @@
# =============================================================================
# check_rtl_text.ps1 —— detect stray non-Verilog text in the RTL sources.
#
# WHY: this project's .v files were twice corrupted by a text tool that inserted a
# line of README prose into them ("Tcl ..." / a Chinese sentence), which made
# Vivado fail with
# ERROR: [Synth 8-2715] syntax error near "..." 'non-printable character 0xe4'
# and produced a cascade of bogus errors in the included ROM .vh. Running this
# script takes a second and catches it before spending a synthesis run.
#
# WHAT IT CHECKS
# 1) a line inside .v/.vh/.xdc that has non-ASCII characters *outside* a comment
# (i.e. real code position) -> corruption;
# 2) a line that looks like pasted prose/markdown (leading "|", "**", or a bare
# "#" that is NOT a Verilog delay/parameter override: #10, #(expr), #delay are
# legal Verilog, so only "#" followed by something else (space, CJK...) is flagged).
#
# USAGE (from the repository root or from pipelined-cpu/):
# powershell -ExecutionPolicy Bypass -File pipelined-cpu\tools\check_rtl_text.ps1
# Exit code 0 = clean, 1 = found something.
# =============================================================================
param(
# -Fix: 自动删除"整行都是粘贴进来的正文"的坏行(保守:只删不含 ; ( ) = 等代码标点的整行)。
# 被部分覆盖的行(例如某个端口行被改坏)只报告,需手工或 git 修。
[switch]$Fix
)
$ErrorActionPreference = 'Stop'
$srcs = Join-Path (Split-Path -Parent $PSScriptRoot) 'srcs'
if (-not (Test-Path $srcs)) { Write-Output "ERROR: cannot find $srcs"; exit 2 }
$bad = 0
$files = Get-ChildItem -Recurse -Path $srcs -Include *.v, *.vh, *.xdc -File
foreach ($f in $files) {
$rel = $f.FullName.Substring($srcs.Length).TrimStart('\', '/')
# 用 .NET 按 UTF-8 读原始字节:PS 5.1 的 Get-Content 会按 GBK 解码,中文会乱。
# 按行切开时保留行尾(\r\n / \n),写回时逐字节保持原样,不归一化换行/编码/BOM。
$bytes = [System.IO.File]::ReadAllBytes($f.FullName)
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
$lines = @([regex]::Split($text, "(?<=\n)"))
$drop = @()
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i].TrimEnd("`r", "`n")
$lineNo = $i + 1
# ---- 只去掉 // (或 .xdc 的 #) 之后的部分,其余按代码位置检查 ----
# 注意:这里不处理 /* */ 块注释(srcs 里没有块注释,之前因此漏报过坏行)
$code = $line
if ($f.Extension -eq '.xdc') {
$h = $code.IndexOf('#'); if ($h -ge 0) { $code = $code.Substring(0, $h) }
}
else {
$c = $code.IndexOf('//'); if ($c -ge 0) { $code = $code.Substring(0, $c) }
}
# 字符串字面量里的中文是合法的(如 $display("中文")),先去掉再判断
$code = $code -replace '"[^"]*"', '""'
$trim = $line.Trim()
# ---- check 1: non-ASCII in code position ----
if ($code -match '[^\x00-\x7F]') {
# 整行只有正文(不含 ; ( ) = ` ' " 之类代码标点)⇒ 可以安全整行删掉
$onlyProse = ($trim -notmatch '[;()=`''"]')
$tag = if ($onlyProse) { ' [整行正文,可用 -Fix 删掉]' } else { ' [部分覆盖,需手工/git 修]' }
Write-Output ("!! $rel`:$lineNo non-ASCII text in code position${tag}:")
Write-Output (" " + $trim)
if ($Fix -and $onlyProse) { $drop += $i }
$bad++
continue
}
# ---- check 2: pasted prose / markdown leftovers ----
# 注意:.v/.vh 里 "#" 是合法的延时语法(#10 / #(expr) / #delay),
# 只有 "#" 后面跟着空格/中文等非代码字符才当成注释残留。
if ($trim -match '^\||^\*\*|^#[^0-9(A-Za-z_]' -and $f.Extension -ne '.xdc') {
Write-Output ("!! $rel`:$lineNo looks like pasted prose:")
Write-Output (" " + $trim)
$bad++
}
}
if ($Fix -and $drop.Count -gt 0) {
$kept = for ($i = 0; $i -lt $lines.Count; $i++) { if ($drop -notcontains $i) { $lines[$i] } }
[System.IO.File]::WriteAllText($f.FullName, ($kept -join ''), (New-Object System.Text.UTF8Encoding($false)))
Write-Output (" -Fix: 已从 $rel 删除 " + $drop.Count + " 行(编码/换行保持不变),请重跑自检确认")
}
}
Write-Output ""
Write-Output ("scanned " + $files.Count + " files under srcs/")
if ($bad -eq 0) {
Write-Output "RTL text check: OK (no stray non-Verilog text found)."
exit 0
}
Write-Output ("RTL text check: FAILED, " + $bad + " suspicious line(s) - fix them before synthesis.")
Write-Output "Hint1: 这类行通常是编辑器/工具粘贴进来的正文;整行正文可以用 -Fix 删掉:"
Write-Output " powershell -ExecutionPolicy Bypass -File tools\check_rtl_text.ps1 -Fix"
Write-Output "Hint2: 若报的是 [部分覆盖],对照 git diff 手工修(被编辑器保存覆盖时,先关掉那个标签页再改)。"
exit 1
+145
View File
@@ -0,0 +1,145 @@
# =============================================================================
# play_saolei.tcl —— 没有 VGA 显示器时,在 xsim 命令行里"玩"扫雷
#
# 用法(建议用 source -encoding utf-8 加载本文件,否则 xsim 默认按 GBK 读,中文注释会乱):
# cd <某个空目录,例如 _tmp/saolei_sim>
# xvlog -d SAOLEI_ROM -i <pipelined-cpu/srcs/board> -i <pipelined-cpu/srcs/rtl> \
# <pipelined-cpu/srcs/rtl/*.v> <pipelined-cpu/srcs/board/cpu21_board_top.v> \
# <pipelined-cpu/srcs/tb/tb_saolei_play.v>
# xelab -d SAOLEI_ROM -top tb_saolei_play -snapshot saolei_play
# xsim saolei_play -tclbatch <本脚本绝对路径>
#
# 然后在 xsim 提示符下敲命令(一个字母就是一步,会立刻重画棋盘):
# k c 左键点击(= 板上 BTNC)
# k u / k d / k l / k r 光标 上/下/左/右(也认 w s a d)
# k f 插旗模式点击(点一下插旗,再点取消;= 按住 SW3 再点)
# k p 只切换插旗模式开关(不点击)
# k z 复位(等于重新上电,回到开始界面)
# k uucc 可以连写:上、上、点、点
# shot 不按键,只重画一遍当前画面 + 存一张 PNG
# view 用系统看图器打开最新那张 PNG
# help 键位说明
#
# 截图:k 每执行**一个**按键动作就自动存一张 PNG 到 shots/NNN_<按键>.png
# (可以直接放进报告),同时覆盖 shots/latest.png。
# 在 VS Code 里打开 shots/latest.png,每次操作后它都会自动刷新 —— 这就是
# "每一步都能看到 PNG" 的用法。
#
# 原理:k 把字符写进 cmd.txt;tb_saolei_play 每 100 us 仿真时间轮询一次并执行按键,
# 同时每 1 ms 把显存 dump 到 now.txt;k 再调 fb2png.py --term 把画面画到终端。
# 仿真速度大约是 1 ms 仿真时间 = 0.5 s 墙钟,所以每步等 1~2 秒,比真实慢很多,
# 但足够用来验证"这个程序真的能玩"。
# =============================================================================
set script_dir [file dirname [file normalize [info script]]]
set fb2png [file normalize [file join $script_dir .. .. saolei tools fb2png.py]]
# 找系统 Python:xsim 的 PATH 里带着 Vivado 自带的 python3.8,直接 exec py 会撞上它
# (SRE module mismatch),所以用绝对路径优先。
proc find_python {} {
foreach ver {314 313 312 311 310} {
set c [file join $::env(LOCALAPPDATA) Programs Python "Python$ver" python.exe]
if {[file exists $c]} { return $c }
}
return "py"
}
set python [find_python]
# xsim 的进程环境里带着 Vivado 的 PYTHONHOME,会让**系统 python** 也去 Vivado 的
# lib 里找模块(SRE module mismatch)。清掉它,子进程才不会继承。
catch { unset ::env(PYTHONHOME) }
catch { unset ::env(PYTHONPATH) }
puts " (python: $python)"
# 按键字符 -> 人类可读的动作(只用于回显)
array set KEYNAME {
c "左键点击" u "上" w "上" d "下" s "下"
l "左" a "左" r "右"
f "插旗点击" p "切插旗开关" g "go 键" z "复位"
}
# 每次只把一个字符写进 cmd.txt(TB 每 100 us 读走一个,读完就清空)
proc send_char {c} {
set fh [open "cmd.txt" "w"]
puts -nonewline $fh $c
close $fh
}
# 先确保 cmd.txt 存在,否则 TB 每 100 us 都会刷一条 could not be opened 警告
if {![file exists "cmd.txt"]} {
set fh [open "cmd.txt" "w"]
close $fh
}
# 把 now.txt 渲染到终端(ANSI 彩色棋盘)
proc render {} {
global fb2png python
if {![file exists "now.txt"]} {
puts " (now.txt not ready yet - run a bit first)"
return
}
if {[catch { exec $python $fb2png --term now.txt >@stdout 2>@stderr } err]} {
puts " render failed: $err"
puts " try manually: py \"$fb2png\" --term now.txt"
}
}
proc help {} {
puts " ---------------- SAOLEI controls (type at the xsim prompt) ----------------"
puts " k c left click (board BTNC)"
puts " k u/d/l/r move cursor up/down/left/right (w/s/a/d work too)"
puts " k f click in flag mode k p toggle flag switch only"
puts " k z reset k uucc several actions at once"
puts " shot redraw+screenshot view open latest PNG"
puts " help this text quit exit"
puts " every key saved one PNG: shots/NNN_<key>.png and shots/latest.png"
puts " ---------------------------------------------------------------------------"
}
# ---- every step saves one PNG: shots/001_c.png ... plus shots/latest.png ----
set shot_dir "shots"
file mkdir $shot_dir
set shot_n 0
proc save_shot {tag} {
global fb2png shot_dir python
if {![file exists "now.txt"]} { return }
incr ::shot_n
set name [format "%s/%03d_%s.png" $shot_dir $::shot_n $tag]
if {[catch { exec $python $fb2png --quiet now.txt $name } err]} {
puts " (screenshot failed: $err)"
return
}
catch { file copy -force $name "$shot_dir/latest.png" }
puts " png -> $name"
}
proc view {} {
global shot_dir
set f [file normalize "$shot_dir/latest.png"]
if {![file exists $f]} { puts " (no screenshot yet)"; return }
catch { exec cmd /c start "" $f }
}
# one step: send the key -> advance simulation -> save PNG -> redraw
proc k {keys} {
set s [string map {" " "c"} [string tolower $keys]]
foreach ch [split $s ""] {
if {[string length $ch] == 0} { continue }
puts " >> $ch"
send_char $ch
run 1500 us
save_shot $ch
}
render
}
proc shot {} {
save_shot "shot"
render
}
# ---- opening: run a little so the start screen is drawn, then show it ----
help
puts "booting... (init + draw start screen)"
run 3 ms
render
+206
View File
@@ -0,0 +1,206 @@
# 扫雷(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 比对”的
一键回归。
+129
View File
@@ -0,0 +1,129 @@
/* -----------------------------------------------------------------------------
* isr.c —— 中断服务程序:从 MMIO 读鼠标事件(**这就是"中断读鼠标操作的指令"**)
*
* 这颗核的中断硬件行为(见 srcs/rtl/cpu21_riscv_irq_ctrl.v):
* * irq_i 上升沿 -> 挂起位置 1;响应中断时硬件自动清挂起位、清 ustatus[0](MIE)、
* 把返回地址压进内部 EPC 栈;
* * 从 uepc 返回用 `uret`(编码 0x00200073,不在标准 C 里,只能内联汇编);
* * **硬件不保存任何通用寄存器**,所以软件必须自己保证不破坏被打断的代码。
*
* 因此这里分了三个固定地址的入口 + 一个纯 C 主体:
* 1) isr1_entry(.text.isr1 = 0x100):**关卡(游戏)入口**,调 level_main()。
* 游戏是一整段会调用别的函数的 C 程序,所以入口把**所有调用者保存寄存器**
* (ra、t0-t6、a0-a7)都存起来,并用 csrrsi 打开 MIE 让鼠标中断能嵌套;
* 2) isr2_entry(.text.isr2 = 0x200):**鼠标读取**,只存 ra 后调下面的 isr_body;
* 3) isr3_entry(.text.isr3 = 0x300):备用,直接 uret;
* 4) isr_body:**纯 C**,本文件用 -ffixed-x1,x5..x7,x10..x17,x28..x31 编译,
* 逼 GCC 只用 s0-s11(callee-saved,函数自己会存/恢复)和 sp,于是被打断的
* 代码的调用者保存寄存器绝对不会被破坏 —— 这样主体就是安全的纯 C 叶子函数。
*
* 主体要做的事:把硬件事件 FIFO(深度 16)里的项全部搬到软件环形队列,
* 主循环再从队列里取。FIFO 读空后中断线自动回 0,下一个事件会重新产生上升沿,
* 所以不会丢事件。
* ---------------------------------------------------------------------------*/
#include "mmio.h"
/* ---- 与 saolei.c 共用的软件事件队列(定义在这里,声明在 mmio.h) ---- */
volatile unsigned g_evq[EVQ_SIZE];
volatile unsigned g_evq_head; /* 主循环消费 */
volatile unsigned g_evq_tail; /* 中断生产 */
volatile unsigned g_evq_lost; /* 队列满丢弃计数(调试) */
/* 中断主体:纯 C 叶子函数(不调用任何函数),只使用 s 寄存器和 sp。
* 编译本文件时必须加 -ffixed-x1 -ffixed-x5..x7 -ffixed-x10..x17 -ffixed-x28..x31
* (见 tools/build.ps1),否则它会用到调用者保存寄存器从而破坏被打断的代码。 */
__attribute__((used, noinline)) void isr_body(void);
void isr_body(void)
{
unsigned ev, next;
/* 硬件 FIFO 非空就一直搬(STATUS[0] = 非空) */
while ((REG_R(REG_STATUS) & 1u) != 0u)
{
ev = REG_R(REG_MDATA); /* lw:{btn[20:16], dy[15:8], dx[7:0]} */
next = (g_evq_tail + 1u) & EVQ_MASK;
if (next != g_evq_head)
{
g_evq[g_evq_tail] = ev;
g_evq_tail = next;
}
else
{
g_evq_lost = g_evq_lost + 1u; /* 软件队列满:丢弃并计数 */
}
REG_W(REG_MACK) = 1u; /* sw:弹出硬件 FIFO 一项 */
}
}
/* ---------------------------------------------------------------------------
* 三个中断入口,分别在链接脚本固定住的 0x100 / 0x200 / 0x300。
*
* IRQ1 = 0x100:**关卡(游戏)入口**。游戏是一整段会调用别的函数的 C 程序,
* 所以入口必须把**所有调用者保存寄存器**(ra、t0-t6、a0-a7)都存起来,
* 否则返回时会把被打断的背景程序弄坏。进入中断时硬件已把 MIE 清 0,这里再
* 重新打开,鼠标中断(IRQ2 优先级更高)才能嵌套进来;uret 时硬件会从内部
* 栈里恢复原来的 MIE。
* IRQ2 = 0x200:**鼠标读取**。C 主体用 -ffixed 编译(只用 s 寄存器和 sp),
* 所以入口只要保存 ra。
* IRQ3 = 0x300:备用(保留给拨码开关等外部源)。
* ------------------------------------------------------------------------- */
extern void level_main(void);
__attribute__((naked, section(".text.isr1"), used, noinline)) void isr1_entry(void)
{
__asm__ volatile(
"addi sp, sp, -64\n\t"
"sw ra, 0(sp)\n\t"
"sw t0, 4(sp)\n\t"
"sw t1, 8(sp)\n\t"
"sw t2, 12(sp)\n\t"
"sw t3, 16(sp)\n\t"
"sw t4, 20(sp)\n\t"
"sw t5, 24(sp)\n\t"
"sw t6, 28(sp)\n\t"
"sw a0, 32(sp)\n\t"
"sw a1, 36(sp)\n\t"
"sw a2, 40(sp)\n\t"
"sw a3, 44(sp)\n\t"
"sw a4, 48(sp)\n\t"
"sw a5, 52(sp)\n\t"
"sw a6, 56(sp)\n\t"
"sw a7, 60(sp)\n\t"
"csrrsi x0, 0x004, 1\n\t" /* ustatus[0]=MIE:允许鼠标中断嵌套 */
"jal ra, level_main\n\t"
"lw a7, 60(sp)\n\t"
"lw a6, 56(sp)\n\t"
"lw a5, 52(sp)\n\t"
"lw a4, 48(sp)\n\t"
"lw a3, 44(sp)\n\t"
"lw a2, 40(sp)\n\t"
"lw a1, 36(sp)\n\t"
"lw a0, 32(sp)\n\t"
"lw t6, 28(sp)\n\t"
"lw t5, 24(sp)\n\t"
"lw t4, 20(sp)\n\t"
"lw t3, 16(sp)\n\t"
"lw t2, 12(sp)\n\t"
"lw t1, 8(sp)\n\t"
"lw t0, 4(sp)\n\t"
"lw ra, 0(sp)\n\t"
"addi sp, sp, 64\n\t"
"uret\n\t");
}
__attribute__((naked, section(".text.isr2"), used, noinline)) void isr2_entry(void)
{
__asm__ volatile(
"addi sp, sp, -8\n\t" /* 保存被打断代码的 ra(jal 会覆盖它) */
"sw ra, 0(sp)\n\t"
"jal ra, isr_body\n\t" /* 调纯 C 主体(它自己保存 s 寄存器) */
"lw ra, 0(sp)\n\t"
"addi sp, sp, 8\n\t"
"uret\n\t"); /* 从 uepc 返回:这颗核的中断返回指令 */
}
__attribute__((naked, section(".text.isr3"), used, noinline)) void isr3_entry(void)
{
__asm__ volatile("uret\n\t");
}
+99
View File
@@ -0,0 +1,99 @@
/* -----------------------------------------------------------------------------
* linker.ld —— 扫雷程序的内存布局
*
* 这颗 CPU 是"哈佛"结构:指令从 ROM 取(instr_addr_o),数据访问 RAM
* (data_addr_o),两个地址空间都从 0 开始,所以这里用两个 MEMORY 区域,
* 都在 0 处开头。
*
* ROM 0x0000_0000 32 KB(= cpu21_board_top 的 ROM_WORDS = 8192 字)
* +0x0000 .text.init 复位入口 _start
* +0x0100 .text.isr1 中断入口 isr1_entry(关卡/游戏,**必须等于 IRQ1_VECTOR**)
* +0x0200 .text.isr2 中断入口 isr2_entry(鼠标读取,必须等于 IRQ2_VECTOR)
* +0x0300 .text.isr3 中断入口 isr3_entry(备用,必须等于 IRQ3_VECTOR)
* +0x0400 .text 其余代码(紧跟 isr3 之后)
*
* RAM 0x0000_0000 4 KB(= saolei_board_top 的 RAM_WORDS = 1024 字)
* +0x0000 .bss 全局变量(全部零初始化,不需要 ROM 里的初值)
* +0x0FFC __stack_top 栈顶(向下增长)
*
* 两个断言很关键(都是这颗 CPU 的硬限制):
* 1) 不能有 .data —— 数据空间读不到 ROM,没有地方放"带初值的变量",
* 所有全局变量必须零初始化;
* 2) 不能有 .rodata —— 同理,程序里不能出现字符串/常量表
* (字模在 RTL 里,不在软件里)。
* ---------------------------------------------------------------------------*/
ENTRY(_start)
MEMORY
{
ROM (rx) : ORIGIN = 0x00000000, LENGTH = 32K
RAM (rwx) : ORIGIN = 0x00000000, LENGTH = 4K
}
SECTIONS
{
/* ------------------------- 数据段(RAM) ------------------------- */
. = 0x00000000;
.bss (NOLOAD) :
{
__bss_start = .;
*(.sbss)
*(.sbss.*)
*(.bss)
*(.bss.*)
*(COMMON)
. = ALIGN(4);
__bss_end = .;
} > RAM
/* 有初值的变量:不允许(见文件头的说明) */
.data : { *(.data) *(.data.*) *(.sdata) *(.sdata.*) } > RAM
/* 只读数据:不允许(没有 auipc / 数据空间读不到 ROM) */
.rodata : { *(.rodata) *(.rodata.*) } > RAM
ASSERT(SIZEOF(.data) == 0, "错误:程序里出现了带初值的全局变量(.data),这颗核没有地方存放它")
ASSERT(SIZEOF(.rodata) == 0, "错误:程序里出现了字符串或常量表(.rodata),这颗核读不到 ROM 里的数据")
/* ------------------------- 代码段(ROM) -------------------------
* 三个中断入口的地址**必须写在段声明里**(.text.isr1 0x100 : ...):
* 只写 ". = 0x100;" 这种位置计数器赋值,在段带 "> ROM" 时会被 ld 忽略,
* 它会按区域顺序把段紧挨着排下去(实测 isr1 掉到了 0x14)。
* 这三个地址要和顶层 cpu21_board_top 的 IRQ1/2/3_VECTOR 参数一致。 */
.text.init 0x00000000 :
{
KEEP(*(.text.init))
} > ROM
.text.isr1 0x00000100 : /* IRQ1:关卡(游戏)入口 */
{
KEEP(*(.text.isr1))
} > ROM
.text.isr2 0x00000200 : /* IRQ2:鼠标读取 */
{
KEEP(*(.text.isr2))
} > ROM
.text.isr3 0x00000300 : /* IRQ3:备用 */
{
KEEP(*(.text.isr3))
} > ROM
.text :
{
*(.text)
*(.text.*)
. = ALIGN(4);
} > ROM
/* ------------------------- 符号 ------------------------- */
__stack_top = ORIGIN(RAM) + LENGTH(RAM) - 4; /* 0x0FFC */
__isr_vector = 0x00000100;
/* .bss 不能长到撞到栈 */
ASSERT(__bss_end < (__stack_top - 0x200),
"错误:.bss 太大,会覆盖栈;请缩小棋盘或加大 RAM_WORDS")
}
+136
View File
@@ -0,0 +1,136 @@
/* -----------------------------------------------------------------------------
* mmio.h —— 扫雷程序的全部 MMIO 地址与编码定义(**只改这一个文件就能换地址**)
*
* 约定:本文件里的所有地址都是"数据空间"的字节地址,和 CPU 的 data_addr_o 一致。
* 写用 *(volatile unsigned *)addr(sw 全字写)或 *(volatile unsigned char *)addr
* (sb 字节写,显存必须用这个)。
*
* -----------------------------------------------------------------------------
* 地址表(与 pipelined-cpu/srcs/rtl/cpu21_mmio.v 里的译码完全一致):
*
* 0x0000_0000 - 0x0000_0FFF 数据 RAM(4 KB,栈 + 全局变量)
* 0x0000_1000 LED 数据寄存器(低 16 位点亮 LED)
* 0x0000_1004 七段数码管:8 位十六进制(低 4 位 = 最右)
* 0x0000_1100 + 0x00 STATUS [0] 事件FIFO非空 [5:1] 项数 [6] 中断使能 [7] 满
* 0x0000_1100 + 0x04 DATA [7:0] dx [15:8] dy [20:16] 按键(读无副作用)
* 0x0000_1100 + 0x08 ACK 写任意值 -> 弹出一次事件
* 0x0000_1100 + 0x0C CTRL [0] 鼠标中断使能
* 0x0000_1100 + 0x10 INPUT [15:0] 拨码开关 [20:16] 按键
* 0x0000_1100 + 0x14 TIME 自由计数器(随机种子)
* 0x0000_1100 + 0x18 FRAME VGA 帧计数(闪烁用)
* 0x0000_1100 + 0x1C EVENTCNT 累计事件数
* 0x0000_1100 + 0x20 LEVEL 写 1 -> 触发"关卡入口"软件中断(IRQ2)
* 0x0000_1100 + 0x24 MAGIC 0xCAFE0002(自检:读得到说明 MMIO 活着)
* 0x0000_1140 + 0x00 DISP_CTRL [0] 光标可见 [6:4] 背景色
* 0x0000_1140 + 0x04 DISP_CURSOR [5:0] 列 [13:8] 行
* 0x0000_1140 + 0x08 DISP_MAGIC 0xCAFE0001
* 0x0000_2000 - 0x0000_2FFF 显存:40 列 x 30 行,每格 2 字节,共 2400 字节
*
* 鼠标事件单位:1/8 格(按键一次 = 8 = 移动一格;PS/2 鼠标用原始计数)。
* 左键点击 -> 事件的 btn 位 0 置 1;右键(PS/2)-> 位 1。
*
* ⚠ 如果你的外设地址和上表不同:只改下面这几个宏,.c 文件不用动。
* ⚠ 三个中断入口必须与 linker.ld 里 .text.isr1/2/3 的地址一致:
* IRQ1(0x100) = 关卡(游戏)入口,IRQ2(0x200) = 鼠标读取,IRQ3(0x300) = 备用。
* 顶层 cpu21_board_top 的 IRQ1/2/3_VECTOR 参数也要跟着改(默认就是这个布局)。
* ---------------------------------------------------------------------------*/
#ifndef SAOLEI_MMIO_H
#define SAOLEI_MMIO_H
/* ---- LED / 七段数码管 ---- */
#define LED_ADDR 0x00001000u
#define REG_SEG 0x00001004u
/* ---- 显存 ---- */
#define FB_BASE 0x00002000u /* 显存起始(字节地址) */
#define FB_PITCH 40u /* 每行 40 格 */
#define FB_ROWS 30u /* 30 行 */
#define GRID_COLS 40u
#define GRID_ROWS 30u
/* ---- 鼠标寄存器组 ---- */
#define REG_STATUS 0x00001100u
#define REG_MDATA 0x00001104u
#define REG_MACK 0x00001108u
#define REG_MCTRL 0x0000110Cu
#define REG_INPUT 0x00001110u
#define REG_TIME 0x00001114u
#define REG_FRAME 0x00001118u
#define REG_EVCNT 0x0000111Cu
#define REG_LEVEL 0x00001120u
#define REG_MMAGIC 0x00001124u
/* ---- 显示控制器寄存器 ---- */
#define REG_CTRL 0x00001140u
#define REG_CURSOR 0x00001144u
#define REG_DMAGIC 0x00001148u
/* ---- 访问宏 ---- */
#define REG_R(a) (*(volatile unsigned *)((unsigned)(a)))
#define REG_W(a) (*(volatile unsigned *)((unsigned)(a)))
/* 显存按字节写(只能 sb,不能 lb —— 这颗核没有 lb/lbu)。每个格子占两个字节。 */
static inline void fb_write_cell(unsigned idx, unsigned glyph, unsigned color)
{
unsigned addr = FB_BASE + (idx << 1);
*(volatile unsigned char *)addr = (unsigned char)(glyph & 0x3Fu);
*(volatile unsigned char *)(addr + 1u) = (unsigned char)(color & 0x07u);
}
/* ---- 格子编码:两个字节 = 字形[5:0] + 颜色[2:0] ---- */
#define G_BLANK 0u /* 空 */
#define G_N1 1u /* 数字 1..8 = 1..8 */
#define G_COVER 9u /* 未揭开 */
#define G_FLAG 10u /* 旗 */
#define G_MINE 11u /* 雷 */
#define G_BOOM 12u /* 爆炸 */
#define G_QUEST 13u /* 问号 */
#define G_BAR 14u /* 横线 */
#define G_SOLID 15u /* 实心 */
/* 大写字母字形编号 16..41,供菜单文字使用。 */
#define G_A 16u
#define G_B 17u
#define G_C 18u
#define G_D 19u
#define G_E 20u
#define G_F 21u
#define G_G 22u
#define G_H 23u
#define G_I 24u
#define G_J 25u
#define G_K 26u
#define G_L 27u
#define G_M 28u
#define G_N 29u
#define G_O 30u
#define G_P 31u
#define G_Q 32u
#define G_R 33u
#define G_S 34u
#define G_T 35u
#define G_U 36u
#define G_V 37u
#define G_W 38u
#define G_X 39u
#define G_Y 40u
#define G_Z 41u
#define C_BLACK 0u
#define C_BLUE 1u
#define C_GREEN 2u
#define C_CYAN 3u
#define C_RED 4u
#define C_MAGENTA 5u
#define C_YELLOW 6u
#define C_GRAY 7u
/* ---- 软件事件环形队列:中断服务程序(isr.c)生产,主循环(saolei.c)消费 ---- */
#define EVQ_SIZE 64u
#define EVQ_MASK (EVQ_SIZE - 1u)
extern volatile unsigned g_evq[EVQ_SIZE];
extern volatile unsigned g_evq_head; /* 主循环消费 */
extern volatile unsigned g_evq_tail; /* 中断生产 */
extern volatile unsigned g_evq_lost; /* 队列满丢弃计数(调试用) */
#endif /* SAOLEI_MMIO_H */
+742
View File
@@ -0,0 +1,742 @@
/* -----------------------------------------------------------------------------
* saolei.c —— 纯 C 扫雷(RV32IM 子集:只用这颗核实现了的指令)
*
* 显示:把每个格子写成显存里的 2 个字节(MMIO 0x2000 起)
* 输入:鼠标事件(中断驱动)——中断里搬到软件队列,主循环在下面处理
*
* 针对这颗 CPU 的硬约束(很重要,改代码时别踩):
* 1) 存储只有 lw / sw / sb,**没有 lb/lh/lbu/lhu** ——
* 所以只按字节"写"显存,从不按字节"读";所有状态都用 32 位字保存。
* 2) **没有 auipc** —— 编译时必须 -fno-pic -mcmodel=medlow,并且不能出现
* 只读数据(.rodata),所以本文件没有任何字符串/常量表(字模在 RTL 里)。
* 3) 乘除只有 mul/mulh/mulhsu/mulhu 和**无符号**的 divu/remu ——
* 所以本文件所有索引运算都用"2 的幂 + 移位/掩码",一次除法都不用。
* 4) 没有 fence/ebreak 等;中断返回是 uret(在 isr.c 里)。
*
* 棋盘:16 x 16,雷数可选 20/40/60(16 是 2 的幂 => 索引全用移位)
* 网格:40 列 x 30 行,棋盘居中放在 (行 7..22, 列 12..27)
* ---------------------------------------------------------------------------*/
#include "mmio.h"
/* 三档难度完全照微软扫雷:初级 9x9/10 雷、中级 16x16/40 雷、高级 30x16/99 雷。
* 棋盘尺寸是运行期变量(三档各不相同),所以位图与队列都按最大尺寸分配。 */
#define BW_MAX 30u /* 最大棋盘宽(高级) */
#define BH_MAX 16u /* 最大棋盘高 */
#define NCELLS_MAX (BW_MAX * BH_MAX) /* 480 */
#define BM_WORDS_MAX ((NCELLS_MAX + 31u) / 32u) /* 15 */
/* 当前难度下的棋盘参数(由 set_difficulty() 设定) */
static unsigned g_bw, g_bh; /* 棋盘宽、高(格) */
static unsigned g_ncells; /* g_bw * g_bh */
static unsigned g_bm_words; /* 位图字数 */
static unsigned g_board_r0; /* 棋盘左上角在网格里的行(居中) */
static unsigned g_board_c0; /* 棋盘左上角在网格里的列(居中) */
/* 格索引 <-> (行, 列)。这颗核有无符号除法 divu/remu,直接用;虽然多周期,
* 但一局游戏这点开销无所谓。 */
#define IDX_R(i) ((i) / g_bw)
#define IDX_C(i) ((i) % g_bw)
/* 游戏状态 */
#define ST_PLAY 0u
#define ST_WIN 1u
#define ST_LOSE 2u
/* ---- 棋盘状态:3 个位图,每个 8 个字(256 位) ---- */
static unsigned mine_bm[BM_WORDS_MAX];
static unsigned open_bm[BM_WORDS_MAX];
static unsigned flag_bm[BM_WORDS_MAX];
static unsigned seen_bm[BM_WORDS_MAX]; /* BFS:已经入过队的格子,避免重复入队把队列撑满 */
/* 其它全局状态(全部进 .bss,靠 start.c 清零) */
static unsigned opened_cnt;
static unsigned flags_cnt;
static unsigned g_state;
static unsigned g_first_done;
static unsigned g_mines;
static unsigned g_difficulty;
static unsigned rnd_state;
static unsigned cur_r, cur_c; /* 光标所在的网格格 */
static int acc_x, acc_y; /* 指针累积量(1/8 格,有符号) */
static unsigned bfs_q[NCELLS_MAX]; /* 揭开用的队列(按最大棋盘分配) */
/* ============================ 小工具 ============================ */
/* 位图取位/置位/清位:i = r*16 + c,全部用移位和掩码 */
static unsigned bget(const unsigned *bm, unsigned i)
{
return (bm[i >> 5] >> (i & 31u)) & 1u;
}
static void bset(unsigned *bm, unsigned i)
{
bm[i >> 5] |= (1u << (i & 31u));
}
static void bclr(unsigned *bm, unsigned i)
{
bm[i >> 5] &= ~(1u << (i & 31u));
}
static void draw_cell(unsigned gr, unsigned gc, unsigned glyph, unsigned color)
{
fb_write_cell(gr * GRID_COLS + gc, glyph, color);
}
/* 把棋盘格 (r,c) 画出来 */
static void draw_tile(unsigned r, unsigned c, unsigned glyph, unsigned color)
{
draw_cell(g_board_r0 + r, g_board_c0 + c, glyph, color);
}
static unsigned num_color(unsigned n)
{
if (n == 1u)
return C_BLUE;
if (n == 2u)
return C_GREEN;
if (n == 3u)
return C_RED;
if (n == 4u)
return C_MAGENTA;
if (n == 5u)
return C_YELLOW;
if (n == 6u)
return C_CYAN;
return C_GRAY;
}
static unsigned in_board(void)
{
if (cur_r < g_board_r0)
return 0u;
if (cur_r >= (g_board_r0 + g_bh))
return 0u;
if (cur_c < g_board_c0)
return 0u;
if (cur_c >= (g_board_c0 + g_bw))
return 0u;
return 1u;
}
static unsigned cur_index(void)
{
return ((cur_r - g_board_r0) * g_bw) + (cur_c - g_board_c0);
}
/* LCG 随机数:只用 mul/add/移位 */
static unsigned rnd_next(void)
{
rnd_state = rnd_state * 1103515245u + 12345u;
return rnd_state >> 16;
}
/* 设定难度:棋盘尺寸 + 雷数 + 居中位置,三档完全照微软扫雷。 */
static void set_difficulty(unsigned difficulty)
{
g_difficulty = difficulty;
if (difficulty == 0u) /* 初级:9x9,10 雷 */
{
g_bw = 9u;
g_bh = 9u;
g_mines = 10u;
}
else if (difficulty == 2u) /* 高级:30x16,99 雷 */
{
g_bw = 30u;
g_bh = 16u;
g_mines = 99u;
}
else /* 中级:16x16,40 雷 */
{
g_bw = 16u;
g_bh = 16u;
g_mines = 40u;
}
g_ncells = g_bw * g_bh;
g_bm_words = (g_ncells + 31u) / 32u;
g_board_c0 = (GRID_COLS - g_bw) / 2u; /* 棋盘居中 */
g_board_r0 = (GRID_ROWS - g_bh) / 2u;
}
/* ============================ 画面 ============================ */
static void clear_screen(void)
{
unsigned i;
for (i = 0u; i < (GRID_COLS * GRID_ROWS); i = i + 1u)
{
fb_write_cell(i, G_BLANK, C_BLACK);
}
}
static void draw_frame(void)
{
unsigned gc, gr;
for (gc = (g_board_c0 - 1u); gc <= (g_board_c0 + g_bw); gc = gc + 1u)
{
draw_cell(g_board_r0 - 1u, gc, G_SOLID, C_CYAN);
draw_cell(g_board_r0 + g_bh, gc, G_SOLID, C_CYAN);
}
for (gr = (g_board_r0 - 1u); gr <= (g_board_r0 + g_bh); gr = gr + 1u)
{
draw_cell(gr, g_board_c0 - 1u, G_SOLID, C_CYAN);
draw_cell(gr, g_board_c0 + g_bw, G_SOLID, C_CYAN);
}
}
static void draw_all_covered(void)
{
unsigned r, c;
for (r = 0u; r < g_bh; r = r + 1u)
{
for (c = 0u; c < g_bw; c = c + 1u)
{
draw_tile(r, c, G_COVER, C_GRAY);
}
}
}
static void update_cursor_reg(void)
{
/* 列用 6 位(0..39):hard 棋盘宽 30,光标列能到 35,掩 31 会把它变成 3 */
REG_W(REG_CURSOR) = (cur_c & 63u) | ((cur_r & 31u) << 8);
}
static void set_bg(unsigned color)
{
REG_W(REG_CTRL) = (color << 4) | 1u; /* bit0 = 光标显示 */
}
/* ============================ 游戏逻辑 ============================ */
static unsigned count_adj(unsigned i)
{
unsigned r = IDX_R(i), c = IDX_C(i), n = 0u, rr, cc;
unsigned r0 = (r == 0u) ? 0u : (r - 1u);
unsigned r1 = (r + 1u >= g_bh) ? (g_bh - 1u) : (r + 1u);
unsigned c0 = (c == 0u) ? 0u : (c - 1u);
unsigned c1 = (c + 1u >= g_bw) ? (g_bw - 1u) : (c + 1u);
for (rr = r0; rr <= r1; rr = rr + 1u)
{
for (cc = c0; cc <= c1; cc = cc + 1u)
{
if ((rr == r) && (cc == c))
continue;
n = n + bget(mine_bm, (rr * g_bw) + cc);
}
}
return n;
}
static void lose(unsigned hit)
{
unsigned k;
for (k = 0u; k < g_ncells; k = k + 1u)
{
if (bget(mine_bm, k))
{
if (k == hit)
{
draw_tile(IDX_R(k), IDX_C(k), G_BOOM, C_RED);
}
else if (!bget(flag_bm, k))
{
draw_tile(IDX_R(k), IDX_C(k), G_MINE, C_RED);
}
}
}
REG_W(LED_ADDR) = 0xDEADu;
g_state = ST_LOSE;
}
static void win(void)
{
unsigned k;
for (k = 0u; k < g_ncells; k = k + 1u)
{
if (bget(mine_bm, k))
{
draw_tile(IDX_R(k), IDX_C(k), G_FLAG, C_YELLOW);
}
}
REG_W(LED_ADDR) = 0xF00Du;
g_state = ST_WIN;
}
static void reveal_at(unsigned i)
{
unsigned qh = 0u, qt = 0u, cur, n, r, c, rr, cc, ni;
unsigned r0, r1, c0, c1, k;
if (bget(open_bm, i) || bget(flag_bm, i))
return;
if (bget(mine_bm, i))
{
lose(i);
return;
}
for (k = 0u; k < g_bm_words; k = k + 1u)
seen_bm[k] = 0u;
bfs_q[qt] = i;
qt = qt + 1u;
bset(seen_bm, i);
while (qh < qt)
{
cur = bfs_q[qh];
qh = qh + 1u;
if (bget(open_bm, cur))
continue;
bset(open_bm, cur);
opened_cnt = opened_cnt + 1u;
n = count_adj(cur);
draw_tile(IDX_R(cur), IDX_C(cur), n, num_color(n));
if (n != 0u)
continue;
/* 空白格:把周围 8 格入队(已入过队的跳过,队列不会溢出) */
r = IDX_R(cur);
c = IDX_C(cur);
r0 = (r == 0u) ? 0u : (r - 1u);
r1 = (r + 1u >= g_bh) ? (g_bh - 1u) : (r + 1u);
c0 = (c == 0u) ? 0u : (c - 1u);
c1 = (c + 1u >= g_bw) ? (g_bw - 1u) : (c + 1u);
for (rr = r0; rr <= r1; rr = rr + 1u)
{
for (cc = c0; cc <= c1; cc = cc + 1u)
{
if ((rr == r) && (cc == c))
continue;
ni = (rr * g_bw) + cc;
if (bget(seen_bm, ni) || bget(open_bm, ni) || bget(flag_bm, ni))
continue;
if (qt < NCELLS_MAX)
{
bset(seen_bm, ni);
bfs_q[qt] = ni;
qt = qt + 1u;
}
}
}
}
if (opened_cnt == (g_ncells - g_mines))
win();
}
static void place_mines(unsigned safe)
{
unsigned placed = 0u, i, r, c;
unsigned sr = IDX_R(safe);
unsigned sc = IDX_C(safe);
while (placed < g_mines)
{
i = rnd_next() % g_ncells; /* 尺寸不再是 2 的幂,用 remu 取模 */
r = IDX_R(i);
c = IDX_C(i);
/* 首点及其 8 邻居不放雷,保证第一次点击一定能展开 */
if ((r + 1u >= sr) && (r <= (sr + 1u)) &&
(c + 1u >= sc) && (c <= (sc + 1u)))
continue;
if (bget(mine_bm, i))
continue;
bset(mine_bm, i);
placed = placed + 1u;
}
}
static void click_here(void)
{
unsigned i;
if (!in_board())
return;
i = cur_index();
if (bget(flag_bm, i))
return;
if (!g_first_done)
{
place_mines(i);
g_first_done = 1u;
}
reveal_at(i);
}
static void toggle_flag(void)
{
unsigned i, r, c;
if (!in_board())
return;
i = cur_index();
if (bget(open_bm, i))
return;
r = cur_r - g_board_r0;
c = cur_c - g_board_c0;
if (bget(flag_bm, i))
{
bclr(flag_bm, i);
flags_cnt = flags_cnt - 1u;
draw_tile(r, c, G_COVER, C_GRAY);
}
else
{
bset(flag_bm, i);
flags_cnt = flags_cnt + 1u;
draw_tile(r, c, G_FLAG, C_YELLOW);
}
REG_W(LED_ADDR) = g_mines - flags_cnt;
}
/* ============================ 鼠标事件 ============================ */
/* 关卡内:光标允许落在盘面外那一圈边框上(左上角那格是"返回") */
static void move_cursor(int dr, int dc)
{
int nr = (int)cur_r + dr;
int nc = (int)cur_c + dc;
if (nr < (int)(g_board_r0 - 1u))
nr = (int)(g_board_r0 - 1u);
if (nr > (int)(g_board_r0 + g_bh))
nr = (int)(g_board_r0 + g_bh);
if (nc < (int)(g_board_c0 - 1u))
nc = (int)(g_board_c0 - 1u);
if (nc > (int)(g_board_c0 + g_bw))
nc = (int)(g_board_c0 + g_bw);
cur_r = (unsigned)nr;
cur_c = (unsigned)nc;
update_cursor_reg();
}
/* ======================= 运行环境状态(背景 / 关卡) =======================
* 程序分成两层,正好对应"伪操作系统 + 关卡"的设想:
* 背景程序 main() 从头跑到尾:画开始界面、等鼠标;
* 关卡程序 level_main() 跑在 IRQ1(0x100) 的中断服务程序里:点"开始"进入,
* 点"返回"就 uret 回到背景被打断的那条指令。
* ========================================================================= */
static unsigned g_in_level; /* 0 = 背景界面,1 = 关卡内 */
static unsigned g_exit; /* 关卡内请求返回 */
/* 背景界面上"开始"按钮的位置(行 20,列 17..22) */
#define START_R 20u
#define START_C0 17u
#define START_C1 22u
#define DIFF_EASY_R 14u
#define DIFF_NORMAL_R 16u
#define DIFF_HARD_R 18u
static void draw_start_screen(void);
/* 背景用:光标可以在整个画面上移动 */
static void move_cursor_screen(int dr, int dc)
{
int nr = (int)cur_r + dr;
int nc = (int)cur_c + dc;
if (nr < 0)
nr = 0;
if (nr > (int)(GRID_ROWS - 1u))
nr = (int)(GRID_ROWS - 1u);
if (nc < 0)
nc = 0;
if (nc > (int)(GRID_COLS - 1u))
nc = (int)(GRID_COLS - 1u);
cur_r = (unsigned)nr;
cur_c = (unsigned)nc;
update_cursor_reg();
}
/* 处理一个鼠标事件:ev = {btn[20:16], dy[15:8], dx[7:0]} */
static void handle_event(unsigned ev)
{
unsigned ux = ev & 0xFFu;
unsigned uy = (ev >> 8) & 0xFFu;
unsigned btn = (ev >> 16) & 31u;
int dx = (ux & 0x80u) ? ((int)ux - 256) : (int)ux;
int dy = (uy & 0x80u) ? ((int)uy - 256) : (int)uy;
unsigned want_flag = (REG_R(REG_INPUT) & 9u) != 0u; /* SW0 或 SW3 = 插旗模式 */
if ((btn & 1u) != 0u)
{ /* 左键 */
if (g_in_level == 0u)
{
if (cur_r == DIFF_EASY_R)
{
set_difficulty(0u);
draw_start_screen();
return;
}
if (cur_r == DIFF_NORMAL_R)
{
set_difficulty(1u);
draw_start_screen();
return;
}
if (cur_r == DIFF_HARD_R)
{
set_difficulty(2u);
draw_start_screen();
return;
}
/* 背景:点到"开始"按钮 -> 拉起软件中断,跳到 IRQ1 里的关卡程序 */
if ((cur_r == START_R) && (cur_c >= START_C0) && (cur_c <= START_C1))
{
g_in_level = 1u; /* 关卡程序返回时自己清 0 */
REG_W(REG_LEVEL) = 1u;
}
}
else if ((cur_r == (g_board_r0 - 1u)) && (cur_c == (g_board_c0 - 1u)))
{
g_exit = 1u; /* 关卡:点左上角黄格 = 返回上一层 */
}
else if (want_flag != 0u)
{
toggle_flag();
}
else
{
click_here();
}
return;
}
if ((btn & 2u) != 0u)
{ /* 右键(PS/2 鼠标有;按键没有) */
if (g_in_level != 0u)
toggle_flag();
return;
}
/* 移动:单位是 1/8 格,累计到 8 就动一格(纯加减比较,没有除法) */
acc_x += dx;
acc_y += dy;
while (acc_x >= 8)
{
if (g_in_level != 0u)
move_cursor(0, 1);
else
move_cursor_screen(0, 1);
acc_x -= 8;
}
while (acc_x <= -8)
{
if (g_in_level != 0u)
move_cursor(0, -1);
else
move_cursor_screen(0, -1);
acc_x += 8;
}
while (acc_y >= 8)
{
if (g_in_level != 0u)
move_cursor(1, 0);
else
move_cursor_screen(1, 0);
acc_y -= 8;
}
while (acc_y <= -8)
{
if (g_in_level != 0u)
move_cursor(-1, 0);
else
move_cursor_screen(-1, 0);
acc_y += 8;
}
}
/* ============================ 背景程序(伪操作系统)============================
* 从头跑到尾:画开始界面 -> 等鼠标 -> 点到"开始"按钮就拉起软件中断。
* ========================================================================== */
static void draw_start_screen(void)
{
unsigned i, selected_color;
clear_screen();
/* 标题和难度菜单使用 RTL 字模中的大写字母。 */
draw_cell(6u, 18u, G_M, C_RED);
draw_cell(6u, 19u, G_I, C_RED);
draw_cell(6u, 20u, G_N, C_RED);
draw_cell(6u, 21u, G_E, C_RED);
draw_cell(6u, 22u, G_S, C_RED);
selected_color = C_YELLOW;
if (g_difficulty != 0u)
selected_color = C_GRAY;
draw_cell(DIFF_EASY_R, 18u, G_E, selected_color);
draw_cell(DIFF_EASY_R, 19u, G_A, selected_color);
draw_cell(DIFF_EASY_R, 20u, G_S, selected_color);
draw_cell(DIFF_EASY_R, 21u, G_Y, selected_color);
selected_color = C_YELLOW;
if (g_difficulty != 1u)
selected_color = C_GRAY;
draw_cell(DIFF_NORMAL_R, 16u, G_N, selected_color);
draw_cell(DIFF_NORMAL_R, 17u, G_O, selected_color);
draw_cell(DIFF_NORMAL_R, 18u, G_R, selected_color);
draw_cell(DIFF_NORMAL_R, 19u, G_M, selected_color);
draw_cell(DIFF_NORMAL_R, 20u, G_A, selected_color);
draw_cell(DIFF_NORMAL_R, 21u, G_L, selected_color);
selected_color = C_YELLOW;
if (g_difficulty != 2u)
selected_color = C_GRAY;
draw_cell(DIFF_HARD_R, 18u, G_H, selected_color);
draw_cell(DIFF_HARD_R, 19u, G_A, selected_color);
draw_cell(DIFF_HARD_R, 20u, G_R, selected_color);
draw_cell(DIFF_HARD_R, 21u, G_D, selected_color);
/* "START" 按钮:绿色实心条,上下各一条黄线框住它。 */
for (i = START_C0; i <= START_C1; i = i + 1u)
{
draw_cell(START_R - 1u, i, G_BAR, C_YELLOW);
draw_cell(START_R, i, G_SOLID, C_GREEN);
draw_cell(START_R + 1u, i, G_BAR, C_YELLOW);
}
/* 按钮上的字母不能用 C_BLACK:VGA 里像素色 = 字模点 ? 字符色 : 背景色,
* 而背景是黑的,所以“黑字”的字形点和空白处全黑 —— 整格完全看不见。
* 这里用黄色:黑底黄字,和上下的黄线框也更呼应。 */
draw_cell(START_R, 17u, G_S, C_YELLOW);
draw_cell(START_R, 18u, G_T, C_YELLOW);
draw_cell(START_R, 19u, G_A, C_YELLOW);
draw_cell(START_R, 20u, G_R, C_YELLOW);
draw_cell(START_R, 21u, G_T, C_YELLOW);
/* 下方提示条 */
for (i = 12u; i <= 27u; i = i + 1u)
{
draw_cell(24u, i, G_BAR, C_CYAN);
}
set_bg(C_BLACK);
cur_r = START_R;
cur_c = (START_C0 + START_C1) / 2u;
update_cursor_reg();
REG_W(LED_ADDR) = 0x0001u; /* LED = 背景状态码 */
REG_W(REG_SEG) = 0u;
}
/* ============================ 关卡(游戏)程序 ============================
* **运行在 IRQ1 的中断服务程序里**(入口 isr1_entry,固定地址 0x100):
* 背景 main() 点"开始" -> 写 LEVEL 寄存器 -> 硬件拉起 irq_i[1]
* -> CPU 跳到 0x100 -> isr1_entry 保存现场、打开 MIE、调用本函数
* -> 这里跑完整一局(鼠标中断 0x200 优先级更高,会嵌套进来把事件放进队列,
* 所以本循环只要轮询队列)
* -> 点左上角黄格 = 返回:清 LEVEL、return -> isr1_entry 恢复现场并 uret
* -> 回到背景程序被打断的那条指令,继续等下一次"开始"。
* ========================================================================== */
/* 关卡里“返回上一层”的黄格 = 棋盘左上角外面一格(随难度尺寸移动) */
void level_main(void)
{
unsigned r;
/* ---- 关卡初始化 ---- */
for (r = 0u; r < g_bm_words; r = r + 1u)
{
mine_bm[r] = 0u;
open_bm[r] = 0u;
flag_bm[r] = 0u;
seen_bm[r] = 0u;
}
opened_cnt = 0u;
flags_cnt = 0u;
g_first_done = 0u;
g_state = ST_PLAY;
g_exit = 0u;
acc_x = 0;
acc_y = 0;
cur_r = g_board_r0 + (g_bh / 2u); /* 光标从棋盘中心开始 */
cur_c = g_board_c0 + (g_bw / 2u);
clear_screen();
draw_frame();
draw_all_covered();
draw_cell(g_board_r0 - 1u, g_board_c0 - 1u, G_BAR, C_YELLOW); /* 左上角黄格 = 返回 */
set_bg(C_BLACK);
update_cursor_reg();
REG_W(LED_ADDR) = g_mines;
REG_W(REG_SEG) = g_mines; /* 数码管显示剩余雷数 */
/* ---- 关卡主循环 ---- */
while (g_exit == 0u)
{
if (g_evq_head != g_evq_tail)
{
unsigned ev = g_evq[g_evq_head];
g_evq_head = (g_evq_head + 1u) & EVQ_MASK;
if (g_state == ST_PLAY)
{
handle_event(ev);
}
else if ((ev >> 16) & 1u)
{
g_exit = 1u; /* 分出胜负后任意点击 = 返回 */
}
}
else if (g_state != ST_PLAY)
{
/* 分出胜负后让背景闪动(用 VGA 帧计数做时间基准,不需要定时器) */
static unsigned phase;
if ((REG_R(REG_FRAME) & 31u) == 0u)
{
phase = phase ^ 1u;
if (phase != 0u)
{
set_bg((g_state == ST_WIN) ? C_GREEN : C_RED);
}
else
{
set_bg(C_BLACK);
}
}
}
}
REG_W(REG_LEVEL) = 0u; /* 清软件中断:否则下次点"开始"没有新的上升沿 */
g_in_level = 0u; /* 回到背景状态 */
draw_start_screen(); /* 恢复开始界面 */
}
/* ============================ 背景主程序 ============================ */
extern void crt_zero_bss(void);
int main(void)
{
crt_zero_bss(); /* 清 .bss(上电 RAM 本来就是 0,保险起见) */
rnd_state = REG_R(REG_TIME) | 1u; /* 用自由计数器当随机种子 */
set_difficulty(1u); /* 默认中级:16x16,40 颗雷 */
REG_W(REG_MCTRL) = 1u; /* 打开鼠标中断(IRQ2,入口 0x200) */
REG_W(REG_LEVEL) = 0u; /* 清关卡软中断 */
g_in_level = 0u;
draw_start_screen();
/* 背景循环:只服务鼠标事件;点"开始"后会被 IRQ1 里的关卡程序"接管",
* 关卡返回时又回到这个循环里继续。 */
for (;;)
{
if (g_evq_head != g_evq_tail)
{
unsigned ev = g_evq[g_evq_head];
g_evq_head = (g_evq_head + 1u) & EVQ_MASK;
handle_event(ev);
}
}
}
+35
View File
@@ -0,0 +1,35 @@
/* -----------------------------------------------------------------------------
* start.c —— C 运行时启动:设置栈指针、把控制权交给 main。
*
* 这是全工程唯一非写不可的内联汇编(C 语言无法表达"设置 sp")。
* 复位时 PC = 0,所以 _start 必须落在 .text.init(见 linker.ld)。
*
* 注意用的是 lui/addi 绝对寻址 + jalr,**不是** auipc —— 这颗核没有 auipc。
* ---------------------------------------------------------------------------*/
extern unsigned __stack_top[];
int main(void);
__attribute__((naked, section(".text.init"), used, noreturn)) void _start(void)
{
__asm__ volatile(
"lui sp, %hi(__stack_top)\n\t"
"addi sp, sp, %lo(__stack_top)\n\t"
"lui t0, %hi(main)\n\t"
"addi t0, t0, %lo(main)\n\t"
"jalr x0, t0, 0\n\t");
}
/* 清 .bss(纯 C)。上电时 FPGA 里的 RAM 本来就是 0,这一步只是保险。 */
extern unsigned __bss_start[];
extern unsigned __bss_end[];
void crt_zero_bss(void)
{
unsigned *p = __bss_start;
while (p < __bss_end)
{
*p = 0u;
p = p + 1u;
}
}
+179
View File
@@ -0,0 +1,179 @@
# =============================================================================
# build.ps1 —— 扫雷程序一键构建:C -> 汇编/目标文件 -> ELF -> .hex + .vh
#
# 用法(在 saolei\ 目录下):
# powershell -ExecutionPolicy Bypass -File tools\build.ps1
# powershell -ExecutionPolicy Bypass -File tools\build.ps1 -Prefix riscv64-unknown-elf-
#
# 产物(都在 saolei\build\ 下):
# saolei.lst objdump -d 反汇编清单(这就是"编译出来的汇编",可以直接看)
# saolei.elf 可执行文件
# saolei.bin 裸二进制
# saolei.hex $readmemh 格式:拷到 pipelined-cpu\srcs\rom\saolei.hex 就能
# 用现成的测试平台(tb_no_intr 等)仿真
# saolei.map 链接 map(看段地址和大小)
# 并且会生成 pipelined-cpu\srcs\board\cpu21_saolei_rom.vh —— 供 cpu21_board_top.v 用
# `include 固化进 ROM(在 Vivado 里定义宏 SAOLEI_ROM 即可切换到扫雷程序)
#
# 构建流程里有三道针对这颗 CPU 的关卡:
# 1) -fno-pic -mcmodel=medlow -mno-relax:避免 auipc / 链接器松弛
# 2) 链接脚本里的 ASSERT:禁止 .data / .rodata(这颗核读不到 ROM 里的数据)
# 3) tools\check_isa.py:逐条检查反汇编,凡是用到未实现指令(auipc/lb/lbu/div...)
# 直接报错并给出修复提示
# =============================================================================
param(
[string]$Prefix = "",
[int]$RomWords = 8192,
[switch]$SkipIsaCheck
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$sw = Join-Path $root 'sw'
$build = Join-Path $root 'build'
New-Item -ItemType Directory -Force $build | Out-Null
# ---------------------------------------------------------------- 工具链探测
if (-not $Prefix) {
foreach ($cand in 'riscv-none-elf-', 'riscv64-unknown-elf-', 'riscv32-unknown-elf-',
'riscv-none-embed-', 'riscv64-linux-gnu-') {
if (Get-Command ($cand + 'gcc') -ErrorAction SilentlyContinue) { $Prefix = $cand; break }
}
}
if (-not $Prefix) {
Write-Output "ERROR: 找不到 RISC-V 交叉编译器(riscv*-gcc)。"
Write-Output ""
Write-Output "装一个即可(推荐 xPack 的 riscv-none-elf-gcc,解压就用,不用管理员权限):"
Write-Output " 1) 打开 https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases"
Write-Output " 2) 下载 win32-x64 的 .zip(例如 xpack-riscv-none-elf-gcc-14.2.0-3-win32-x64.zip)"
Write-Output " 3) 解压到例如 C:\tools\riscv-none-elf-gcc"
Write-Output " 4) 把 C:\tools\riscv-none-elf-gcc\bin 加进 PATH(或者直接运行本脚本时):"
Write-Output " powershell -ExecutionPolicy Bypass -File tools\build.ps1 -Prefix 'C:\tools\riscv-none-elf-gcc\bin\riscv-none-elf-'"
exit 1
}
$GCC = $Prefix + 'gcc'
$OBJDUMP = $Prefix + 'objdump'
$OBJCOPY = $Prefix + 'objcopy'
$SIZE = $Prefix + 'size'
$Python = 'py'
if (-not (Get-Command $Python -ErrorAction SilentlyContinue)) {
$Python = 'python'
}
Write-Output "工具链前缀: $Prefix"
# ---------------------------------------------------------------- 编译参数
# -march=rv32im_zicsr : RV32I + M 扩展 + Zicsr(CSR 指令:csrrw/csrrsi/csrrci)。
# GCC 15 起不再默认给 rv32im 开 zicsr,中断入口里的
# csrrsi 必须显式打开这个扩展才认。
# -mcmodel=medlow -fno-pic : 用 lui/addi 绝对寻址,不产生 auipc
# **必须放开链接器松弛**(不要 -mno-relax):gcc 的函数调用默认是
# auipc ra, %pcrel_hi(f) + jalr ra, ra, %pcrel_lo(f)
# 要靠链接器松弛把它变成一条 jal,否则 auipc 会留在最终代码里(这颗核没有)。
# 松弛只会在两种情况下改变指令:把 pcrel 调用变成 jal,或在使用 gp 相对寻址时
# 变成 gp 相对访问 —— 后者已被 -msmall-data-limit=0 从根上禁止。
# -msmall-data-limit=0 : 不用 gp 相对寻址
# -fno-builtin -nostdlib -ffreestanding : 不要 libc(没有环境,也没有 printf)
# -fno-jump-tables : 不生成跳转表(跳转表会落在 .rodata,这颗核读不到)
$ccCommon = @(
'-march=rv32im_zicsr', '-mabi=ilp32',
'-mcmodel=medlow',
'-fno-pic', '-fno-PIC', '-fno-pie',
'-msmall-data-limit=0', '-fno-common',
'-fno-builtin', '-fno-stack-protector', '-fno-jump-tables',
'-ffreestanding', '-nostdlib',
'-ffunction-sections', '-fdata-sections',
'-Os', '-Wall', '-Wextra'
)
# 中断主体(isr.c)必须只使用 callee-saved 寄存器,否则会破坏被打断的代码。
# 下面把所有"调用者保存"的通用寄存器都固定住,GCC 就只能用 s0-s11 和 sp。
$fixedRegs = @()
foreach ($r in 1, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 16, 17, 28, 29, 30, 31) {
$fixedRegs += ('-ffixed-x' + $r)
}
# ---------------------------------------------------------------- 编译
$objs = @()
# 跑外部命令并在非 0 退出码时立刻停下(native 命令失败不会抛异常,必须查 $LASTEXITCODE)
function Invoke-Tool {
param([string]$Exe, [string[]]$ArgList, [string]$What)
& $Exe @ArgList
if ($LASTEXITCODE -ne 0) {
Write-Output ("ERROR: 失败 -> " + $What)
exit 1
}
}
Write-Output "编译 start.c ..."
Invoke-Tool -Exe $GCC -ArgList ($ccCommon + @('-c', (Join-Path $sw 'start.c'), '-o', (Join-Path $build 'start.o'))) -What 'start.c'
$objs += (Join-Path $build 'start.o')
Write-Output "编译 saolei.c ..."
Invoke-Tool -Exe $GCC -ArgList ($ccCommon + @('-c', (Join-Path $sw 'saolei.c'), '-o', (Join-Path $build 'saolei.o'))) -What 'saolei.c'
$objs += (Join-Path $build 'saolei.o')
Write-Output "编译 isr.c(额外加 -ffixed 约束)..."
Invoke-Tool -Exe $GCC -ArgList ($ccCommon + $fixedRegs + @('-c', (Join-Path $sw 'isr.c'), '-o', (Join-Path $build 'isr.o'))) -What 'isr.c'
$objs += (Join-Path $build 'isr.o')
# ---------------------------------------------------------------- 链接
$elf = Join-Path $build 'saolei.elf'
$map = Join-Path $build 'saolei.map'
$ld = Join-Path $sw 'linker.ld'
Write-Output "链接 ..."
# 注意:PowerShell 会把未加引号的 "-Wl,--no-relax" 当成参数对解析(逗号引起),
# 所以这里统一用参数数组,所有选项都带引号。
$linkArgs = @(
'-march=rv32im_zicsr', '-mabi=ilp32', '-nostdlib', '-nostartfiles',
"-Wl,-Map=$map",
'-T', $ld, '-o', $elf
) + $objs
Invoke-Tool -Exe $GCC -ArgList $linkArgs -What '链接(-T sw/linker.ld)'
# ---------------------------------------------------------------- 反汇编 + ISA 检查
$lst = Join-Path $build 'saolei.lst'
& $OBJDUMP -d -M no-aliases $elf | Out-File -Encoding utf8 $lst
Write-Output "反汇编清单: $lst"
$isaOk = $true
if (-not $SkipIsaCheck) {
Write-Output ""
& $Python (Join-Path $PSScriptRoot 'check_isa.py') $lst
if ($LASTEXITCODE -ne 0) { $isaOk = $false }
}
# ---------------------------------------------------------------- 转 hex / vh
$bin = Join-Path $build 'saolei.bin'
$hex = Join-Path $build 'saolei.hex'
$vh = Join-Path $root '..\pipelined-cpu\srcs\board\cpu21_saolei_rom.vh'
& $OBJCOPY -O binary $elf $bin
& $Python (Join-Path $PSScriptRoot 'elf2hex.py') $bin $hex $RomWords $vh
# ---------------------------------------------------------------- 汇总
Write-Output ""
Write-Output "==== 段大小 ===="
& $SIZE $elf
$isrLine = Select-String -Path $lst -Pattern '^[0-9a-f]+ <(isr1_entry|isr2_entry|isr3_entry|_start)>:' |
Select-Object -ExpandProperty Line
Write-Output "==== 入口地址 ===="
foreach ($l in $isrLine) { Write-Output (" " + $l) }
Write-Output " 三个中断入口必须和顶层 cpu21_board_top 的参数一致:"
Write-Output " linker.ld 的 .text.isr1/2/3 = IRQ1/2/3_VECTOR = 0x100/0x200/0x300"
Write-Output ""
if ($isaOk) {
Write-Output "构建完成:$hex 与 $vh 已生成。"
Write-Output " 仿真:把 saolei.hex 拷到 pipelined-cpu\srcs\rom\saolei.hex,用 tb_no_intr 加载"
Write-Output " 上板:cpu21_saolei_rom.vh 已生成到 srcs/board/,Vivado 里定义宏 SAOLEI_ROM 后综合"
} else {
Write-Output "构建完成,但 ISA 检查没通过(见上面的清单),这个 hex 放到 CPU 上会跑飞。"
exit 1
}
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""check_isa.py —— 校验反汇编清单里没有这颗 CPU 未实现的指令。
用法: python tools/check_isa.py <objdump输出.txt>
为什么需要它:本 CPU(srcs/rtl/cpu21_riscv_decoder.v)只实现了 RV32I 的一个
子集,最容易踩的三个坑是:
* 没有 auipc -> 编译器一旦生成它,整段代码就废了(多为 PIC/rodata 引起)
* 没有 lb/lh/lbu/lhu -> 只要碰 char/short 类型的读就会出现
* 没有有符号 div/rem(只有 divu/remu) -> 有符号除法/取余会被静默算错
所以这里用"白名单"方式检查:出现名单外的助记符就报错并给出修复提示。
退出码 = 违规条数(>255 时截断),0 表示通过。
"""
import sys
# 这颗 CPU 真正实现了的指令(见 cpu21_riscv_decoder.v)
ALLOWED = {
# R 型
"add", "sub", "sll", "slt", "sltu", "xor", "srl", "sra", "or", "and",
# M 扩展(只有这几个)
"mul", "mulh", "mulhsu", "mulhu", "divu", "remu",
# I 型
"addi", "slti", "sltiu", "xori", "ori", "andi", "slli", "srli", "srai",
# 访存:只有 lw / sw / sb(没有 lb/lh/lbu/lhu/sh)
"lw", "sw", "sb",
# 控制流
"beq", "bne", "blt", "bge", "bltu", "bgeu", "jal", "jalr",
# 高位立即数
"lui",
# 系统
"ecall", "uret", "csrrw", "csrrsi", "csrrci",
}
# objdump 有时仍打印伪指令;这些展开后都是上面支持的指令
ALIAS_OK = {
"nop", "mv", "li", "j", "jr", "ret", "neg", "not",
"beqz", "bnez", "blez", "bgez", "bltz", "bgtz",
"seqz", "snez", "sltz", "sgtz",
}
# 违规时给出针对性提示
HINTS = {
"auipc": "出现了 auipc:说明编译带了 PIC/PIE 或访问了只读数据。"
"请用 -fno-pic -mcmodel=medlow,并去掉字符串/常量表(.rodata)",
"lb": "没有 lb:不要用 char/short 类型读内存,改用 32 位字",
"lh": "没有 lh:同上",
"lbu": "没有 lbu:不要用 unsigned char/short 类型读内存(写显存可以用 sb,但别读回来)",
"lhu": "没有 lhu:同上",
"sh": "没有 sh:只用 sw / sb",
"div": "没有有符号 div:改用 unsigned 运算(unsigned 除法会编成 divu)",
"rem": "没有有符号 rem:改用 unsigned 运算(unsigned 取余会编成 remu)",
"divw": "不支持 64 位运算",
"remw": "不支持 64 位运算",
"call": "call 会展开成 auipc+jalr:请直接用普通函数调用(jal)",
"tail": "tail 会展开成 auipc:请用普通跳转",
"fence": "没有 fence/fence.i",
"ebreak": "没有 ebreak",
"mret": "这颗核的中断返回是 uret,不是 mret",
"sret": "这颗核的中断返回是 uret",
"wfi": "没有 wfi:主循环只能用轮询",
"unimp": "出现了 unimp(非法指令/内联汇编占位)",
}
def main():
if len(sys.argv) < 2:
print("用法: python tools/check_isa.py <objdump输出.txt>")
return 2
path = sys.argv[1]
total = 0
violations = []
with open(path, encoding="utf-8", errors="replace") as f:
for line in f:
if "\t" not in line:
continue
parts = line.split("\t")
if len(parts) < 3 or not parts[0].strip().endswith(":"):
continue
mnemonic = parts[2].strip().split()[0]
if not mnemonic:
continue
total += 1
if (mnemonic not in ALLOWED) and (mnemonic not in ALIAS_OK):
violations.append((parts[0].strip(), mnemonic, line.rstrip()))
print("检查了 %d 条指令。" % total)
if not violations:
print("ISA 检查通过:全部指令都在本 CPU 的实现范围内。")
return 0
print("")
print("!! ISA 检查失败:有 %d 条指令这颗 CPU 没有实现:" % len(violations))
seen = set()
for addr, mnemonic, raw in violations[:40]:
print(" %s %s" % (addr, raw.strip()))
seen.add(mnemonic)
if len(violations) > 40:
print(" ...(只列出前 40 条)")
print("")
print("修复提示:")
for m in sorted(seen):
print(" * %s: %s" % (m, HINTS.get(m, "不在本 CPU 的指令白名单里")))
return min(len(violations), 255)
if __name__ == "__main__":
sys.exit(main())
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""elf2hex.py —— 把 objcopy 出来的裸二进制转成 ROM 需要的两份文件。
用法: python tools/elf2hex.py <input.bin> <output.hex> [ROM_WORDS] [output.vh]
产出:
1) <output.hex> 每行一个 32 位字($readmemh 格式,供 srcs/rom/ 下的
测试平台加载,例如拷成 srcs/rom/saolei.hex)
2) <output.vh>(可选) rom[i] = 32'h...; 语句,供
pipelined-cpu/srcs/board/cpu21_board_top.v 里 `include
固化进片内 ROM(未用到的地址填 NOP 0x00000013)
(未用到的地址填 NOP 0x00000013)
字序:输入的 .bin 是按字节小端排列的机器码,这里按小端拼成 32 位字。
"""
import sys
NOP = 0x00000013
def main():
if len(sys.argv) < 3:
print("用法: python tools/elf2hex.py <input.bin> <output.hex> [ROM_WORDS] [output.vh]")
return 2
src = sys.argv[1]
dst = sys.argv[2]
words_total = int(sys.argv[3]) if len(sys.argv) > 3 else 8192
vh_path = sys.argv[4] if len(sys.argv) > 4 else None
with open(src, "rb") as f:
data = f.read()
if len(data) % 4 != 0:
print("警告:二进制长度 %d 不是 4 的倍数,尾部会被补 0" % len(data))
data += b"\x00" * (4 - len(data) % 4)
words = []
for i in range(0, len(data), 4):
words.append(int.from_bytes(data[i:i + 4], "little"))
# 段间空洞填 NOP:objcopy 出来的 .bin 在 .text.isr1 / .text.isr2 / .text.isr3
# 这些"绝对地址段"之间会留下 0x00000000,而本 CPU 把 0x00000000 译成自跳指令
# (beq x0,x0,0 形式),PC 一旦落进去就永远出不来(仿真里已复现:PC 热点全在
# 0x194-0x1FF / 0x218-0x2FF 这些空洞地址上)。这里把"最后一条非零字之前"的
# 0 字统一换成 NOP,让 PC 即使误入也能顺势走过。
last_nz = -1
for i, w in enumerate(words):
if w != 0:
last_nz = i
filled = 0
for i in range(last_nz):
if words[i] == 0:
words[i] = NOP
filled += 1
if len(words) > words_total:
print("错误:程序 %d 个字,超过 ROM 容量 %d 个字(改大 ROM_WORDS 或精简程序)"
% (len(words), words_total))
return 1
with open(dst, "w", encoding="ascii") as f:
for w in words:
f.write("%08X\n" % w)
if vh_path:
with open(vh_path, "w", encoding="utf-8") as f:
f.write(" // 由 tools/elf2hex.py 生成,请勿手工编辑\n")
f.write(" // 源文件:%s(%d 个字,其余填 NOP)\n" % (src, len(words)))
for addr in range(words_total):
w = words[addr] if addr < len(words) else NOP
f.write(" rom[%d] = 32'h%08X;\n" % (addr, w))
print("已生成 %s(%d 个字)%s" % (dst, len(words),
("、%s(%d 个字,NOP 填充)" % (vh_path, words_total)) if vh_path else ""))
return 0
if __name__ == "__main__":
sys.exit(main())
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env python3
"""fb2png.py —— 把 tb_saolei 导出的显存 dump 还原成 PNG(等价于 VGA 截图)。
用法:
py saolei/tools/fb2png.py <dump.txt> [输出.png]
py saolei/tools/fb2png.py --term <dump.txt> # 只在终端打印彩色棋盘(交互玩扫雷用)
py saolei/tools/fb2png.py --quiet <dump.txt> [输出.png] # 只写 PNG,不打印任何东西
dump 格式(tb_saolei.v 里的 dump_fb 任务生成):
CURSOR <row> <col>
CTRL 0x<byte> # [0] 光标可见 [6:4] 背景色
FB 2400 # 随后 2400 行十六进制字节:每格 2 字节(字形、颜色)
渲染依据(与 RTL 严格对应):
* 字库:直接从 cpu21_vga.v 的 font_row() 解析(8x8、bit7 最左,放大 2 倍 = 16x16 一格)
—— RTL 改了字库(比如新增 A-Z)这里会自动跟随,不用手工抄表
* 调色板 cpu21_vga.v 的 pal():3 位 -> 4:4:4 RGB(0xA 分量 = 170)
* 显存每格 2 字节:byte0 = 字形[5:0],byte1 = 颜色[2:0]
* 光标 cpu21_vga.v 的 cursor_box:格子四边一圈 2 像素宽的黄色框(常亮)
* 画面 40x30 格 -> 640x480
另外还会在终端打印一份 ASCII 图,方便在没有图片查看器的地方快速看结果。
"""
import os
import re
import struct
import sys
import zlib
COLS, ROWS = 40, 30
CELL_PX = 16 # 8x8 字模放大 2 倍
FB_BYTES = COLS * ROWS * 2 # 每格 2 字节(字形 + 颜色)= 2400
RTL_VGA = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "..", "pipelined-cpu", "srcs", "rtl",
"cpu21_vga.v"))
def load_font_from_rtl(path=None):
"""直接从 cpu21_vga.v 的 font_row() 里解析 8x8 字模,返回 {字形号: [8 行]}。
RTL 里每个字形形如
6'd16: font_row = (r == 3'd0) ? 8'h7C : (r == 3'd1) ? 8'h66 : ... ;
或全部相同行时形如
6'd15: font_row = 8'hFF;
两种情况都能解析。
"""
path = path or RTL_VGA
try:
with open(path, encoding="utf-8", errors="replace") as f:
text = f.read()
except OSError as e:
raise SystemExit("读不到 RTL 字库 %s:%s" % (path, e))
m = re.search(r"function\s*\[7:0\]\s*font_row.*?endfunction", text, re.S)
if not m:
raise SystemExit("%s 里找不到 font_row()" % path)
font = {}
parts = re.split(r"6'd(\d+)\s*:", m.group(0))
for i in range(1, len(parts) - 1, 2): # 奇数下标是字形号,后一格是它的分支体
idx, seg = int(parts[i]), parts[i + 1]
rows = [0] * 8
pairs = re.findall(r"\(r == 3'd(\d+)\)\s*\?\s*8'h([0-9A-Fa-f]{2})", seg)
if pairs:
for k, v in pairs:
rows[int(k)] = int(v, 16)
else: # 形如 "font_row = 8'hFF;":8 行一样
one = re.search(r"font_row\s*=\s*8'h([0-9A-Fa-f]{2})", seg)
if one:
rows = [int(one.group(1), 16)] * 8
font[idx] = rows
if not font:
raise SystemExit("%s 的 font_row() 没解析出任何字形" % path)
return font
# --- 调色板(3 位 -> RGB),抄自 cpu21_vga.v 的 pal(),4 位分量按 *17 扩到 8 位 ---
PAL = [
(0x0, 0x0, 0x0), # 0 黑
(0x0, 0x0, 0xA), # 1 深蓝
(0x0, 0xA, 0x0), # 2 绿
(0x0, 0xA, 0xA), # 3 青
(0xA, 0x0, 0x0), # 4 红
(0xA, 0x0, 0xA), # 5 洋红
(0xA, 0xA, 0x0), # 6 黄
(0xA, 0xA, 0xA), # 7 浅灰
]
GLYPH_CH = {0: ' ', 9: '#', 10: 'P', 11: '*', 12: 'X', 13: '?', 14: '-', 15: '='}
COLOR_CH = {0: '.', 1: 'b', 2: 'g', 3: 'c', 4: 'r', 5: 'm', 6: 'y', 7: 'w'}
def write_png(path, width, height, rows):
"""rows: 每行一个 bytes(长度 width*3),最小 PNG 编码。"""
raw = b"".join(b"\x00" + r for r in rows)
def chunk(tag, data):
return (struct.pack(">I", len(data)) + tag + data
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0))
png += chunk(b"IDAT", zlib.compress(raw, 9))
png += chunk(b"IEND", b"")
with open(path, "wb") as f:
f.write(png)
def parse_dump(path):
cur = (None, None)
ctrl = 0
fb = []
with open(path, encoding="utf-8", errors="replace") as f:
for line in f:
line = line.strip()
if line.startswith("CURSOR"):
p = line.split()
cur = (int(p[1]), int(p[2]))
elif line.startswith("CTRL"):
ctrl = int(line.split()[1], 16)
elif line.startswith("FB"):
fb = []
elif len(line) == 2 and fb is not None:
try:
fb.append(int(line, 16))
except ValueError:
pass
if len(fb) != FB_BYTES:
raise SystemExit("显存字节数不对:%d(应为 %d;新版每格 2 字节)"
% (len(fb), FB_BYTES))
return cur, ctrl, fb
def cell_of(fb, idx):
"""第 idx 格 -> (字形, 颜色):byte0 = 字形[5:0],byte1 = 颜色[2:0]。"""
return fb[2 * idx] & 0x3F, fb[2 * idx + 1] & 0x07
def cell_char(g):
"""字形号 -> 一个可见字符(1..8 数字,16..41 = A-Z,其余见 GLYPH_CH)。"""
if 16 <= g <= 41:
return chr(ord('A') + g - 16)
if 1 <= g <= 8:
return str(g)
return GLYPH_CH.get(g, '?')
def dump_note(path):
"""把 dump 开头的 TIME / CURSOR / STATS 拼成一行,当终端画面的标题。"""
note = []
try:
with open(path, encoding="utf-8", errors="replace") as f:
for line in f:
if line.startswith(("TIME", "CURSOR", "STATS")):
note.append(line.strip())
elif line.startswith("FB"):
break
except OSError:
pass
return " ".join(note)
def draw_term(fb, cur, cursor_box, note=""):
"""在终端用 ANSI 24 位颜色画 40x30 棋盘(每格一个字符),光标格反色。"""
try: # Windows 控制台默认不是 UTF-8
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
out = ["\x1b[H\x1b[2J"] # 回左上角 + 清屏(连续刷新不闪屏残留)
if note:
out.append(" " + note + "\n")
out.append(" " + "".join(str(x % 10) for x in range(COLS)) + "\n")
for y in range(ROWS):
cells = []
for x in range(COLS):
g, c = cell_of(fb, y * COLS + x)
r, gg, b = PAL[c]
ch = cell_char(g)
if cursor_box and (x, y) == cur:
cells.append("\x1b[7m\x1b[38;2;%d;%d;%dm%s" % (r * 17, gg * 17, b * 17,
ch if ch != ' ' else '_'))
else:
cells.append("\x1b[38;2;%d;%d;%dm%s" % (r * 17, gg * 17, b * 17, ch))
out.append("%2d " % y + "".join(cells) + "\x1b[0m\n")
out.append("\x1b[0m")
out.append("图例: #=未揭开 P=旗 *=雷 X=爆炸 A-Z=文字 1..8=数字 空格=已揭开的空地;反色格 = 光标\n")
sys.stdout.write("".join(out))
sys.stdout.flush()
def main():
args = list(sys.argv[1:])
term_only = "--term" in args
quiet = "--quiet" in args
if term_only:
args.remove("--term")
if quiet:
args.remove("--quiet")
if quiet: # 只出图不说话(给脚本批量截图用)
sys.stdout = open(os.devnull, "w")
if not args:
raise SystemExit(__doc__)
src = args[0]
dst = args[1] if len(args) > 1 else src.rsplit(".", 1)[0] + ".png"
cur, ctrl, fb = parse_dump(src)
font = load_font_from_rtl()
bg = (ctrl >> 4) & 0x7
cursor_box = bool(ctrl & 1) and cur[0] is not None
if term_only:
draw_term(fb, cur, cursor_box, dump_note(src))
return
print("光标 = (%s, %s) 光标可见 = %s 背景色 = %d"
% (cur[0], cur[1], bool(ctrl & 1), bg))
# ---- 渲染 ----
rows = []
for y in range(ROWS * CELL_PX):
row = bytearray()
cy, fy = y // CELL_PX, (y % CELL_PX) // 2
for x in range(COLS * CELL_PX):
cx, fx = x // CELL_PX, (x % CELL_PX) // 2
g, c = cell_of(fb, cy * COLS + cx)
px_on = (font.get(g, [0] * 8)[fy] >> (7 - fx)) & 1
col = c if px_on else bg
if cursor_box and (cx, cy) == cur:
# 光标格:四边 2 像素黄色框,与 cpu21_vga.v 的 cursor_box 一致
ox, oy = x % CELL_PX, y % CELL_PX
if ox < 2 or ox > CELL_PX - 3 or oy < 2 or oy > CELL_PX - 3:
col = 6
r, gg, b = PAL[col]
row += bytes((r * 17, gg * 17, b * 17))
rows.append(bytes(row))
write_png(dst, COLS * CELL_PX, ROWS * CELL_PX, rows)
print("已生成 %s(%dx%d)" % (dst, COLS * CELL_PX, ROWS * CELL_PX))
# ---- 终端 ASCII 图(彩色版;PNG 已经写好了,这里只是方便快速看一眼)----
draw_term(fb, cur, cursor_box, dump_note(src))
main()