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
30 changed files with 6355 additions and 1971 deletions
+1
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/
+258 -94
View File
@@ -13,20 +13,20 @@ risc-v/
├─ README.md 本文档
├─ pipelined-cpu/ Vivado 工程目录(也是源文件所在地)
│ ├─ create_project.tcl 一键重建工程的 Tcl 脚本
│ ├─ srcs/rtl/*.v 处理器与外设 RTL(17 个文件,cpu21_riscv_redirect_int_bpb.v 为处理器顶层)
│ ├─ 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(可选 PS/2 鼠标)
│ │ 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 三个测试平台
│ ├─ 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 汇编器)
│ │ rars_f0c874c.jar(课程提供的 RARS 汇编器)、play_saolei.tcl(命令行玩扫雷)
│ └─ programs/ 汇编源程序与期望值:跑马灯 demo、指令集/MMIO 测试、课程原始
│ 资产(risc-v-benchmark_ccab、真值表),以及中断综合测试程序
│ risc-v-intrupt-ccab.asm / .hex
@@ -69,18 +69,18 @@ risc-v/
表中的 `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 |
| 指令类别 | 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` 分支逻辑。
@@ -91,13 +91,13 @@ risc-v/
这些地址都在 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`–`0x27FF` | 显存 | 40 列 × 30 行、每格 1 字节(字形 + 颜色),写端口接到板级顶层的双口 RAM | `lw` 组合读回 |
| 地址 | 外设 | 写 | 读 |
| --------------------------- | -------------- | ------------------------------------------------------------------------------------ | ---------------------------- |
| `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 是否活着。
@@ -147,34 +147,39 @@ risc-v/
## 文件与模块划分
| 文件 | 模块 | 职责 |
| --------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------- |
| `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/board/cpu21_board_top.v` | `cpu21_board_top` | 板级顶层:时钟分频、复位同步、片内 ROM/RAM、显存双口 RAM、VGA/数码管/鼠标/中断连线 |
| `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/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…;` 初始化语句) |
| 文件 | 模块 | 职责 |
| ----------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------- |
| `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 下对拆分前后逐周期对比,波形输出与最终统计(周期、停顿、气泡、分支
@@ -249,19 +254,29 @@ risc-v/
队列 + 优先级选择器逐个上报(每拍最多一个事件,不丢键)。事件单位是 1/8 格,
软件端只用整数加减就能换算成格(不需要除法)。
- **`cpu21_ps2_mouse`**:可选的真鼠标,只实现接收:`ps2_clk` 下降沿把 11 位帧
移位进来(起始位 + 8 数据 + 奇校验 + 停止位),校验后按 3 字节包解析出
dx/dy/按键,产生 `ev_valid_o` 脉冲。
**按位的下标**存进 `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`**:把这些接起来并裁到 23 个引脚。内含参数化分频器
- **`cpu21_board_top`**:把这些接起来,只引出板上真正需要的引脚。内含参数化分频器
(`CLK_DIV` / `CLK_DIV_INTRUPT`)、复位同步器(异步置位 + 同步释放)、片内
ROM(组合读、`nop` 填充、`ifdef` 链选程序)与 RAM(字节写使能 + 组合读、
只响应 4 KB 以内地址,避免和 MMIO 别名)、显存双口 RAM(CPU 写口 + VGA 读口
同处一个时钟域,用像素使能而不是第二时钟)、数码管、VGA、鼠标源选择
(`USE_PS2_MOUSE`)、中断连线(SW0/1/2 分别与鼠标事件/关卡软件中断相或)、
同处一个时钟域,用像素使能而不是第二时钟)、数码管、VGA、PS/2 键鼠(同一根
PS/2 线上同时挂鼠标与键盘两个解码器,用 `hid_sel`(SW15) 选哪个生效,板载 5 向
按键始终有效)、中断连线(SW0/1/2 分别与鼠标事件/关卡软件中断相或)、
锁存式中断指示 LED(请求上升沿置位、对应级别的 ISR 执行 `uret` 后熄灭)和
`led_we` 写入指示。
`led_we` 写入指示;另有一个“PS/2 诊断”模式把收到的字节数/时钟活动/线电平
摆到 LED 高位(`DEBUG_PS2_RX_LED`,见下文“PS/2 键鼠”)。
## 上板(Nexys4DDR + Vivado)
@@ -276,14 +291,70 @@ BPB/中断观测值)。如果直接把它设为实现顶层,这些端口会
### 引脚分配(`cpu21_board_top.xdc`)
| 端口 | 引脚 | 板上元件 | 说明 |
| ------------- | ----------- | ------------------------- | ------------------------------------------------------------------- |
| `clk100` | E3 | 100 MHz 晶振 | 经 `CPU_DIV` 分频后给 CPU(默认 50 MHz) |
| `rst_n` | C12 | 按键 CPU_RESETN(低有效) | 异步置位、与 CPU 时钟同步释放 |
| 端口 | 引脚 | 板上元件 | 说明 |
| --------------- | ----------- | ------------------------- | ----------------------------------------------------------------------- |
| `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 | 三路中断源,拨动即触发(内部同步并锁存) |
| `led[15:0]` | H17…V11 | LED0–LED15 | `LED[2:0]` = 三路中断指示(锁存,ISR 返回后熄灭),`LED[15:3]` 熄灭 |
| `led_we` | R12 | LED16_B | MMIO 写入指示,每写一次拉长约 0.34 s @50 MHz |
| `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`)
@@ -297,11 +368,11 @@ BPB/中断观测值)。如果直接把它设为实现顶层,这些端口会
因此在 `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 ✓ | 改造前的设置,现在没必要了 |
| 配置 | 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 中必须用
@@ -327,13 +398,13 @@ create_generated_clock -name cpu_clk -source [get_ports { clk100 }] \
### 分频与刷新率一览(想改速度看这里)
| 想改什么 | 改哪里 | 当前值 / 换算 |
| -------------------------- | ----------------------------------------------- | -------------------------------------------------------- |
| 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 |
| 想改什么 | 改哪里 | 当前值 / 换算 |
| -------------------------- | --------------------------------------------------- | ------------------------------------------------------------ |
| 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,能看到
@@ -364,12 +435,12 @@ create_generated_clock -name cpu_clk -source [get_ports { clk100 }] \
板级顶层用一条 `` `ifdef `` 链决定 ROM 里固化哪个程序(连带决定中断入口地址),
`create_project.tcl` 里的两个开关就是给这条链用的:
| 宏(`sources_1` 的 Verilog Define) | ROM 文件 | 中断入口 `IRQ1/2/3` | 说明 |
| ----------------------------------- | ------------------------------------ | --------------------------- | ------------------------------------------ |
| 宏(`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(默认) |
| `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`
@@ -389,7 +460,6 @@ ROM 里 ISR 的起点一致**,所以换程序时宏与入口是一起换的:
```
换回跑马灯就是 `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` 指向头文件目录:
@@ -399,7 +469,6 @@ ROM 里 ISR 的起点一致**,所以换程序时宏与入口是一起换的:
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`,与宏无关。
@@ -460,10 +529,10 @@ xsim board_sim -tclbatch run_quit.tcl
每步之间用软件循环延时约 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 |
| `pipelined-cpu/srcs/board/cpu21_board_rom_fast.vh` | 仅仿真:一次跑完整个跑马灯序列 | 2.6 µs |
```powershell
# 快速版仿真:编译时加上 -d BOARD_ROM_FAST(xvlog 与 xelab 都要带)
@@ -495,12 +564,12 @@ Vivado 工程 `pipelined-cpu/pipelined-cpu.xpr` 被 `.gitignore` 排除,需要
`xc7a100tcsg324-1` 上的实现结果(乘除法改为多周期单元、`CLK_DIV=2` = 50 MHz):
| 项目 | 结果 |
| -------- | -------------------------------------------------------------------------------------------- |
| 综合 | `synth_design Complete!` |
| 项目 | 结果 |
| -------- | -------------------------------------------------------------------------------------------------- |
| 综合 | `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` |
| 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 倍),并且乘除法不再位于关键路径上。
@@ -542,6 +611,80 @@ CPU 时钟 6.25 MHz → 50 MHz(快 8 倍),并且乘除法不再位于关
并用 `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/` 目录下运行:
@@ -563,12 +706,33 @@ powershell -ExecutionPolicy Bypass -File tools\run_regress.ps1
期望结果(2026-09,多周期乘除法 + `CLK_DIV=2`):
| 测试 | 期望 |
| -------------------------------------------------- | ---------------------------------------------------------------------------- |
| muldiv_test / isa_test / lui_test / mmio_test | 13 / 24 / 4 / 5 个 LED 值全部匹配 |
| 测试 | 期望 |
| -------------------------------------------------- | ------------------------------------------------------------------------------ |
| 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 |
| 板级(快速 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 一键重建)
+90 -14
View File
@@ -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,10 +51,62 @@ 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 等生成目录)后重跑本脚本。"
return 1
# ---------------------------------------------------------------- 清理旧工程
# 一键重建:先删掉旧工程和所有生成目录。工程完全由 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
}
}
# ---------------------------------------------------------------- 建工程
@@ -61,7 +117,8 @@ if {[file exists "$origin_dir/$proj_name.xpr"]} {
# create_project $proj_name $origin_dir -part $part_name \
# -board_part digilentinc.com:nexys4ddr:part0:1.1
# (那样工程会记上板卡,别人没装板卡文件就打不开了,所以默认不这么写。)
create_project $proj_name $origin_dir -part $part_name
# -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"]
@@ -71,8 +128,13 @@ add_files -norecurse -fileset sources_1 [list \
set_property file_type {Verilog Header} \
[get_files "$origin_dir/srcs/board/cpu21_board_rom.vh"]
# 默认跑哪个内置程序:中断测试程序优先,其次是扫雷,最后是走马灯 demo。
# 修改下面两个开关即可切换板上程序;同一时间只应启用一个。
# 默认跑哪个内置程序(同一时间只应启用一个):
# 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
@@ -85,6 +147,15 @@ if {[file exists $intrupt_ccab_rom]} {
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"
@@ -95,9 +166,12 @@ if {[file exists $saolei_rom]} {
puts "NOTE: 还没有 $saolei_rom —— 需要先跑 saolei/tools/build.ps1 生成"
}
# 关键:sources_1 定义 SAOLEI_ROM,cpu21_board_top.v 里的 `ifdef 链才会用扫雷 ROM
# (该宏只影响综合用的 sources_1,仿真 fileset 仍各自用 tb 自带的 ROM)
if {$use_intrupt_ccab && [file exists $intrupt_ccab_rom]} {
# 关键: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]} {
@@ -142,7 +216,9 @@ foreach fs {sim_1 sim_2} {
# ---------------------------------------------------------------- 结果汇总
puts "工程已创建:$origin_dir/$proj_name.xpr"
puts " sources_1 顶层 = cpu21_board_top(上板:时钟分频 + 片内 ROM/RAM + 引脚)"
if {$use_intrupt_ccab && [file exists $intrupt_ccab_rom]} {
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)}
+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,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
+143 -30
View File
@@ -15,6 +15,8 @@
// rst_n (C12) -> 复位按键 CPU_RESETN,低有效,经复位同步器
// go (N17) -> 按键 BTNC:ecall 停机后按下继续运行
// irq_sw[2:0] -> 拨码开关 SW[2:0]:3 路中断源
// 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] 熄灭
@@ -31,10 +33,10 @@
// 注意:ROM_WORDS / RAM_WORDS 需为 2 的幂(默认 1024,对应地址位 [11:2])。
// -----------------------------------------------------------------------------
module cpu21_board_top #(
parameter [31:0] LED_ADDR = 32'h0000_1000,
parameter ROM_WORDS = 8192,
parameter RAM_WORDS = 1024,
parameter [31:0] USTATUS_INIT = 32'h0000_0001,
parameter [31:0] LED_ADDR = 32'h0000_1000,
parameter ROM_WORDS = 8192,
parameter RAM_WORDS = 1024,
parameter [31:0] USTATUS_INIT = 32'h0000_0001,
// 板载 100 MHz 的分频比(1 = 直用 100 MHz,2 = 50 MHz,…)。
// 乘除法改成多周期单元后,原来 ~94 ns 的组合乘法/除法已经不在关键路径上,
// 实测(xc7a100tcsg324-1):
@@ -43,7 +45,7 @@ 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,数码管上根本读不出来。停留时间与分频比成正比,例如:
@@ -51,9 +53,22 @@ module cpu21_board_top #(
// 32 → 3.125 MHz(约 160 ms)、64 → 1.5625 MHz(约 320 ms)。
// 取大于 2 的值只是把实际时钟变慢,XDC 的 cpu_clk 仍按最坏情况 50 MHz 约束,
// 所以不会掩盖时序违例(约束偏严)。
parameter integer CLK_DIV_INTRUPT = 32,
// 鼠标事件源:0 = 板上 5 向按键(免外设,默认);1 = PS/2 鼠标(Pmod JA)
parameter integer USE_PS2_MOUSE = 0
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,低有效
@@ -64,6 +79,7 @@ module cpu21_board_top #(
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 写入指示
// 七段数码管(共阳极:位选/段选都是低有效)
@@ -86,6 +102,9 @@ module cpu21_board_top #(
// 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;
@@ -154,6 +173,8 @@ module cpu21_board_top #(
wire [31:0] data_wdata;
wire [ 3:0] data_wstrb;
wire data_we;
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;
// 扫雷扩展外设的连线(先声明,例化在下面)
@@ -170,7 +191,14 @@ module cpu21_board_top #(
wire frame_tick;
wire [15:0] sw_vec = {12'b0, sw_flag, irq_sw};
// 中断连线:SW0 与鼠标共用一个入口,SW1 与"关卡入口"共用一个入口。
wire [2:0] irq_vec = {irq_sw[2], (level_irq | irq_sw[1]), (mouse_irq | irq_sw[0])};
// 中断线对应关系(必须与 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:组合读(未用到的地址填 NOP)
@@ -184,9 +212,13 @@ module cpu21_board_top #(
// 扫雷程序(由 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"
@@ -234,6 +266,11 @@ module cpu21_board_top #(
.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),
@@ -315,14 +352,41 @@ module cpu21_board_top #(
end
end
assign led = {{13{1'b0}}, irq_led_q};
// ---- 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)
// (12.5 MHz 下约 1.36 s)
else if (led_we_cnt != 24'd0) led_we_cnt <= led_we_cnt - 24'd1;
end
@@ -331,11 +395,11 @@ module cpu21_board_top #(
// ===========================================================================
// 扫雷外设:显存 + VGA + 七段数码管 + 鼠标事件源 + 中断连线
//
// 显存是一块 1200 字节的双口 RAM(40x30 格,每格 1 字节):CPU 通过 MMIO 的
// 显存是一块 2400 字节的双口 RAM(40x30 格,每格 2 字节):CPU 通过 MMIO 的
// fb_* 写端口写入,VGA 用另一个读端口组合读出。两者同处 clk_cpu 域(VGA 的
// 25 MHz 像素节拍由使能信号产生,不是独立时钟),因此没有跨时钟域问题。
// ===========================================================================
localparam integer FB_WORDS = 300; // 300 字 = 1200 字节
localparam integer FB_WORDS = 600; // 600 字 = 2400 字节,40x30 格每格 2 字节
// ---- 显存 ----
@@ -395,12 +459,40 @@ module cpu21_board_top #(
.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 (
@@ -410,27 +502,48 @@ module cpu21_board_top #(
.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 ()
.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)
);
end else begin : g_btn
cpu21_pointer #(
.SAMPLE_DIV(50000)
) u_ptr (
.clk (clk_cpu),
.reset (reset),
.btn_i (btn_vec),
.ev_valid_o(ev_valid),
.ev_dx_o (ev_dx),
.ev_dy_o (ev_dy),
.ev_btn_o (ev_btn)
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
@@ -63,6 +63,9 @@ 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] }]
@@ -97,6 +100,9 @@ set_property -dict { PACKAGE_PIN D8 IOSTANDARD LVCMOS33 } [get_ports { vga_b[
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 鼠标(可选:Pmod JA 插 Pmod PS2,顶层 USE_PS2_MOUSE=1)
set_property -dict { PACKAGE_PIN C17 IOSTANDARD LVCMOS33 PULLUP true } [get_ports { ps2_clk }]
set_property -dict { PACKAGE_PIN D18 IOSTANDARD LVCMOS33 PULLUP true } [get_ports { ps2_data }]
## 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 }]
+223 -252
View File
@@ -11,7 +11,7 @@
// 0x0000_1004 七段数码管:8 位十六进制数(每 4 位一个数码管,低 4 位 = 最右)
// 0x0000_1100 + 4*k 鼠标寄存器组(见下)
// 0x0000_1140 + 4*k 显示控制器寄存器(见下)
// 0x0000_2000 - 0x27FF 显存窗口:40x30 格、每格 1 字节(只用到前 1200 字节)
// 0x0000_2000 - 0x2FFF 显存窗口:40x30 格、每格 2 字节(共 2400 字节)
//
// 鼠标寄存器(基址 0x1100):
// +0x00 STATUS RO [0] FIFO 非空 [5:1] 项数 [6] 中断使能 [7] FIFO 满
@@ -35,276 +35,247 @@
// 同一块 RAM),读操作把 fb_rdata_i 组合回读数据,保持"组合读"的时序不变。
//
// 两个中断源:
// mouse_irq_o = 中断使能 && 事件 FIFO 非空(接 irq_i[0])
// level_irq_o = LEVEL 寄存器(软件中断,接 irq_i[1],用来"进入关卡")
// 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 integer FB_WORDS = 300,
parameter integer FIFO_DEPTH = 16
) (
input wire clk,
input wire reset,
// MEM 级的访存请求(来自 EX/MEM 流水寄存器)
input wire mem_valid_i,
input wire mem_write_i,
input wire mem_read_i,
input wire [31:0] addr_i,
input wire [31:0] wdata_i,
input wire [ 3:0] wstrb_i,
// MMIO 读回
output wire [31:0] rdata_o,
output wire rdata_hit_o,
// LED 外设(原样保留)
output reg [31:0] led_data_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 位寄存器内的任意字节偏移都算命中)
// ---------------------------------------------------------------------------
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];
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,
// MEM 级的访存请求(来自 EX/MEM 流水寄存器)
input wire mem_valid_i,
input wire mem_write_i,
input wire mem_read_i,
input wire [31:0] addr_i,
input wire [31:0] wdata_i,
input wire [ 3:0] wstrb_i,
// MMIO 读回
output wire [31:0] rdata_o,
output wire rdata_hit_o,
// LED 外设(原样保留)
output reg [31:0] led_data_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 位寄存器内的任意字节偏移都算命中)
// ---------------------------------------------------------------------------
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];
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 - 0x27FF(addr[12]=1 且 addr[11]=0)
wire sel_fb = (addr_i[31:13] == 19'd0) && addr_i[12] && !addr_i[11];
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[10:2];
wire fb_ok = (fb_widx < FB_WORDS);
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;
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 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;
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 <= 1'b0;
if (led_wr) begin
led_data_o <= merge_bytes(wdata_i, led_data_o, wstrb_i);
led_valid_o <= 1'b1;
end
end
// 字节写合并: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
// ---------------------------------------------------------------------------
// 七段数码管: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
// ---------------------------------------------------------------------------
// 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 <= 1'b0;
if (led_wr) begin
led_data_o <= merge_bytes(wdata_i, led_data_o, wstrb_i);
led_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
// ---------------------------------------------------------------------------
// 七段数码管: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] rdata_mux;
// ---------------------------------------------------------------------------
// 显示控制器寄存器(显存本体在板级顶层)
// ---------------------------------------------------------------------------
reg [31:0] disp_ctrl_q, disp_cursor_q;
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
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
assign rdata_o = rdata_mux;
// ---------------------------------------------------------------------------
// 读数据(组合,与"指令存储器组合读/数据存储器组合读"的时序一致)
// ---------------------------------------------------------------------------
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
+8 -4
View File
@@ -76,16 +76,20 @@ module cpu21_pointer #(
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:上
sel = 5'b01000; // BTNU:上
else if (pend_next[2])
sel = 5'b00010; // BTND:下
sel = 5'b00100; // BTND:下
else if (pend_next[1])
sel = 5'b00001; // BTNL:左
sel = 5'b00010; // BTNL:左
else if (pend_next[0])
sel = 5'b00100; // BTNR:右
sel = 5'b00001; // BTNR:右
end
always @(posedge clk or posedge reset) begin
+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
+29 -18
View File
@@ -4,17 +4,23 @@
// -----------------------------------------------------------------------------
// cpu21_ps2_mouse —— 可选的真鼠标输入:PS/2 鼠标(Digilent Pmod PS2 插在 JA 上)。
//
// JA[1] = PS/2 DATA,JA[2] = PS/2 CLK(两条线都需要上拉,见 XDC 里的 PULLUP)。
// JA[1] = PS/2 DATA,JA[2] = PS/2 CLK(两条线都需要上拉)。当前 XDC 把 ps2_clk /
// ps2_data 接到板载 USB HID 口的 PS/2 侧(F4 / B2),也可以改回 JA(C17 / D18)。
//
// 注意:本模块没有在真实硬件上验证过(作者手头没有 PS/2 鼠标),如果你不用真
// 鼠标,请让顶层用 cpu21_pointer(5 向按键)作为事件源即可,C 程序完全不用改。
// 板级顶层 cpu21_board_top 用参数 USE_PS2_MOUSE 选择这两者。
// 注意:接收/解析逻辑(包括帧对齐与奇校验)已由 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 为符号位;
// byte2 = dy,bit7 为符号位;dx 正 = 右,**dy 正 = 上**(PS/2 的原生方向),
// 而软件里正 dy = 下,所以上报前把 dy 取负,让鼠标和板载按键/键盘方向一致;
// 3) 上报:当 dx/dy 非 0 或按键状态变化时产生一个事件(单位与
// cpu21_pointer 相同,即原始计数 = 1/8 格);
// 4) 本模块只做"接收",不实现主机→鼠标的命令发送(0xF4 使能上报)。多数
@@ -76,9 +82,9 @@ module cpu21_ps2_mouse (
reg [7:0] dx_q, dy_q;
reg [4:0] btn_q; // {4'b0, middle, right, left} -> 只取低 3 位使用
// 校验:11 位中 1 的个数应为奇数(奇校验)
function automatic odd_parity_ok(input [10:0] f);
odd_parity_ok = ^{f[9:1]}; // data + parity 共 9 位,1 的个数应为偶数
// 校验: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
@@ -95,17 +101,20 @@ module cpu21_ps2_mouse (
ev_valid_o <= 1'b0;
if (clk_fall) begin
if (bit_cnt_q == 4'd0 && ps2_data_s2 == 1'b0) begin
// 起始位有效,开始收
frame_q <= {ps2_data_s2, frame_q[10:1]};
bit_cnt_q <= 4'd1;
// 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 if (bit_cnt_q != 4'd0) begin
frame_q <= {ps2_data_s2, frame_q[10:1]};
else begin
frame_q[bit_cnt_q] <= ps2_data_s2;
if (bit_cnt_q == 4'd10) begin
bit_cnt_q <= 4'd0;
// f = {停止, 校验, 8 数据, 起始},这里只关心数据与校验
if (odd_parity_ok(frame_q) && (frame_q[10] == 1'b1)) begin
// 停止位就是刚采到的这一位(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
@@ -123,8 +132,10 @@ module cpu21_ps2_mouse (
end
8'd2: begin
byte_cnt_q <= 8'd0;
ev_dx_o <= pkt_q[15:8]; // byte1 = dx
ev_dy_o <= frame_q[8:1]; // byte2 = dy
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;
@@ -48,7 +48,11 @@ 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,
@@ -69,7 +73,7 @@ module cpu21_riscv_redirect_int_bpb #(
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..299)
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, // 显存读回(组合)
@@ -479,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),
+263 -176
View File
@@ -4,7 +4,7 @@
// -----------------------------------------------------------------------------
// cpu21_vga —— 640x480@60 的"格子"显示控制器(显存端口接板级顶层的双口 RAM)。
//
// CPU 不直接操作像素:它只通过 MMIO 往显示缓冲区里写"每格 1 字节"(字形 + 颜色),
// CPU 不直接操作像素:它只通过 MMIO 往显示缓冲区里写"每格 2 字节"(字形 + 颜色),
// 本模块按像素扫描把格子展开成画面(8x8 字模放大 2 倍填满 16x16 的格子)。
// 这样一格只要 1 次字节写,刷屏开销极小。
//
@@ -14,227 +14,314 @@
// 25 MHz 的像素节拍。**不引入第二个时钟域**,因此显示缓冲区的读(本模块)
// 与写(MMIO,同一时钟)之间没有跨时钟域问题。
//
// 光标:由 CURSOR 寄存器的 (x,y) 决定,在对应格子上画一个白色闪动的方框
// 光标:由 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
);
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
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;
// ---------------------------------------------------------------------------
// 扫描计数
// ---------------------------------------------------------------------------
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
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);
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)));
// 同步信号在 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
// ---------------------------------------------------------------------------
// 格子索引与缓冲区读出
// ---------------------------------------------------------------------------
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:2]; // 字地址(1 字 = 4 格)
assign fb_raddr_o = cidx[10:1]; // 字地址(1 字 = 2 格)
wire [1:0] lane = cidx[1:0];
wire [31:0] cell_word = fb_word_i;
wire [7:0] cell_byte = cell_word >> {lane, 3'b000};
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 [3:0] glyph = cell_byte[3:0];
wire [2:0] gcolor = cell_byte[6:4];
// 全局背景色(字模点为 0 时显示的颜色)
wire [2:0] bgcolor = ctrl_i[7:4];
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 的格子
// ---------------------------------------------------------------------------
wire [2:0] fx = hx[3:1];
wire [2:0] fy = vy[3:1];
// ---------------------------------------------------------------------------
// 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 [3:0] g;
input [2:0] r;
begin
case (g)
4'd0:
font_row = 8'h00; // 空白
4'd1:
font_row = (r == 3'd0) ? 8'h18 : (r == 3'd1) ? 8'h38 : (r == 3'd2) ? 8'h18 :
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
4'd2:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h06 :
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
4'd3:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h06 :
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
4'd4:
font_row = (r == 3'd0) ? 8'h0C : (r == 3'd1) ? 8'h1C : (r == 3'd2) ? 8'h3C :
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
4'd5:
font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h60 : (r == 3'd2) ? 8'h7C :
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
4'd6:
font_row = (r == 3'd0) ? 8'h1C : (r == 3'd1) ? 8'h30 : (r == 3'd2) ? 8'h60 :
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
4'd7:
font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h0C :
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
4'd8:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 :
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
4'd9:
font_row = (r == 3'd0) ? 8'hFF : (r == 3'd1) ? 8'h81 : (r == 3'd2) ? 8'hBD :
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; // 未揭开的方块
4'd10:
font_row = (r == 3'd0) ? 8'h40 : (r == 3'd1) ? 8'h70 : (r == 3'd2) ? 8'h78 :
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; // 旗
4'd11:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h7E : (r == 3'd2) ? 8'hFF :
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; // 雷
4'd12:
font_row = (r == 3'd0) ? 8'h81 : (r == 3'd1) ? 8'h42 : (r == 3'd2) ? 8'h24 :
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; // 爆炸
4'd13:
font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h06 :
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; // 问号
4'd14:
font_row = (r == 3'd3) ? 8'hFF : (r == 3'd4) ? 8'hFF : 8'h00; // 横线
4'd15:
font_row = 8'hFF; // 实心
default:
font_row = 8'h00;
endcase
end
endfunction
wire [7:0] frow = font_row(glyph, fy);
wire glyph_px = frow[3'd7 - fx]; // 1 = 画字模点
// ---------------------------------------------------------------------------
// 光标框(闪动)
// ---------------------------------------------------------------------------
reg [24:0] blink_q;
always @(posedge clk or posedge reset) begin
if (reset)
blink_q <= 25'd0;
else if (tick)
blink_q <= blink_q + 25'd1;
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
wire blink = blink_q[24];
endfunction
wire cursor_here = (cursor_i[4:0] == ccol) && (cursor_i[12:8] == crow);
wire on_border = (hx[3:0] == 4'd0) || (hx[3:0] == 4'd15) ||
(vy[3:0] == 4'd0) || (vy[3:0] == 4'd15);
wire cursor_box = cursor_here && blink && ctrl_i[0] && on_border;
wire [7:0] frow = font_row(glyph, fy);
wire glyph_px = frow[3'd7-fx]; // 1 = 画字模点
// ---------------------------------------------------------------------------
// 颜色(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
// ---------------------------------------------------------------------------
// 光标框:黄色、常亮、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;
wire [2:0] px_color = glyph_px ? gcolor : bgcolor;
wire [2:0] px_final = cursor_box ? 3'd7 : px_color;
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
// ---------------------------------------------------------------------------
// 颜色(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
+1
View File
@@ -38,6 +38,7 @@ module tb_board_top;
.btnl (1'b0),
.btnr (1'b0),
.sw_flag (1'b0),
.hid_sel (1'b0),
.ps2_clk (1'b1),
.ps2_data(1'b1),
.an (),
+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
+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()
+6 -3
View File
@@ -11,8 +11,9 @@
# 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 line
# that is bare text after `endmodule`).
# 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
@@ -71,7 +72,9 @@ foreach ($f in $files) {
}
# ---- check 2: pasted prose / markdown leftovers ----
if ($trim -match '^\||^\*\*|^#[^0-9#]' -and $f.Extension -ne '.xdc') {
# 注意:.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++
+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
+52 -30
View File
@@ -19,7 +19,8 @@ saolei/
配套硬件在 Vivado 工程里(不在本目录):`srcs/rtl/cpu21_mmio.v`(外设中枢)、
`cpu21_vga.v`(显示)、`cpu21_seg7.v`(数码管)、`cpu21_pointer.v`(按键当鼠标)、
`cpu21_ps2_mouse.v`(可选真鼠标)、`srcs/board/cpu21_board_top.v`(上板顶层)。
`cpu21_ps2_mouse.v` / `cpu21_ps2_kbd.v`(可选真鼠标/真键盘)、
`srcs/board/cpu21_board_top.v`(上板顶层)。
---
@@ -126,13 +127,14 @@ powershell -ExecutionPolicy Bypass -File tools\build.ps1
| `0x0000_1004` | 七段数码管:8 位十六进制数(低 4 位 = 最右) |
| `0x0000_1100+` | 鼠标:STATUS / DATA / ACK / CTRL / INPUT / TIME / FRAME / EVCNT / LEVEL / MAGIC |
| `0x0000_1140+` | 显示控制器:CTRL(光标可见、背景色)/ CURSOR(列、行)/ MAGIC |
| `0x0000_2000-0x27FF` | 显存:40 列 × 30 行、每格 1 字节(只用到前 1200 字节) |
| `0x0000_2000-0x2FFF` | 显存:40 列 × 30 行、每格 2 字节(共 2400 字节) |
`LEVEL` 是**软件中断**寄存器:写 1 拉起 `irq_i[1]`(关卡入口),关卡返回前写 0 清掉,
否则下次点"开始"没有新的上升沿。`MAGIC`(`0xCAFE0001/2`)可以读来自检 MMIO 是否活着。
格子编码 1 字节 = `[3:0] 字形 [6:4] 颜色 [7] 保留`;字形 0 空白、1..8 数字、9 未揭开、
10 旗、11 雷、12 爆炸、13 问号、14 横线、15 实心;颜色是 3 位调色板。
格子编码为两个字节:第一个字节低 6 位是字形编号,第二个字节低 3 位是颜色;字形 0 空白、
1..8 数字、9 未揭开、10 旗、11 雷、12 爆炸、13 问号、14 横线、15 实心、16..41 为 A-Z;
颜色是 3 位调色板。
## 5. 怎么跑起来
@@ -147,38 +149,58 @@ copy build\saolei.hex <仓库>\pipelined-cpu\srcs\rom\saolei.hex
`cpu21_board_top.v` 就会加载 `cpu21_saolei_rom.vh`;不定义仍是原来的跑马灯程序。
接 VGA 显示器,按 `CPU_RESETN` 复位 → 出现开始界面(标题 + 绿色按钮 + 闪动光标框)。
**操作**(默认用板上 5 向按键):
**操作**(板载 5 向按键一直有效):
| 操作 | 按键 |
| ------------------------------------ | --------------------------------------- |
| 移动光标 | `BTNU` / `BTND` / `BTNL` / `BTNR` |
| 左键(揭开;在开始界面上 = 开始) | `BTNC` |
| 插旗模式(拨上去后左键 = 插/取消旗) | `SW0` 或 `SW3` |
| 返回上一层(关卡内) | 把光标移到盘面**左上角黄格**再按 `BTNC` |
| 操作 | 板载按键 | PS/2 设备(SW15 选) |
| ------------------------------------ | ------------------------------------- | ------------------------------------- |
| 移动光标 | `BTNU` / `BTND` / `BTNL` / `BTNR` | 键盘 `↑↓←→`(按住 = 连续)/ 鼠标移动 |
| 左键(揭开;开始界面 = 选难度/开始) | `BTNC` | 键盘 `Enter`/空格 / 鼠标左键 |
| 右键(插旗 / 取消旗) | `SW3`(或 `SW0`)拨上 + `BTNC` | 键盘左 `Shift`/`F`/`C` / 鼠标右键 |
| 返回上一层(关卡内) | 光标移到盘面**左上角黄格**再按左键 | 同上 |
**鼠标**:默认按键;想用真鼠标就把顶层参数 `USE_PS2_MOUSE` 设为 1(需要 Digilent
Pmod PS2 插在 JA + 一个 PS/2 鼠标)。`cpu21_ps2_mouse.v` 只实现了**接收**(发 0xF4
初始化要靠鼠标产生的时钟脉冲,不是固定延时能糊的,所以没写假的);多数鼠标上电即上报,
需要 0xF4 的那种得自己补。USB 鼠标在这个核上不现实(协议栈放不下)。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 工具链链路 | **已实测**:编译/链接/ISA 检查/hex 生成/入口地址逐条核对 全部通过 |
| `saolei.c` / `isr.c` / `start.c` / `linker.ld` | 逻辑对着 RTL 写;**未在仿真器/硬件上跑过** |
| `cpu21_mmio.v` 扩展、`cpu21_vga.v`、`cpu21_seg7.v`、`cpu21_pointer.v`、板级顶层、XDC | **未综合、未上板** |
| 部分 | 状态 |
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| 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` 跑行为仿真,看有没有往 `0x2000` 起的显存写字节、
按键后有没有跳进 `0x200`;③ 再上板。
建议验证顺序:① 跑 `pipelined-cpu/tools/run_regress.ps1` 确认跑马灯/中断/指令测试仍全
PASS;② 定义 `SAOLEI_ROM` 跑行为仿真(`tb_saolei` 会自动打完一局并 dump 显存);
③ 上板看 VGA;④ 接键鼠时先用 `srcs/tb/tb_ps2_*` 确认解码无误,再看 LED 诊断确认板载
桥接芯片到底有没有转发你插的那个设备。
## 7. 已知可改进项
- **屏幕上没有字母**:字模是 4 位字形(0-15),所以"开始"是绿色按钮条 + 光标框而不是
"START" 字样。要显示文字就把格子编码扩成 5 位字形(`mmio.h` 的 `CELL()` 与
`cpu21_vga.v` 的字模表各改一处,颜色降到 3 位)。
- 棋盘固定 16×16、40 雷(16 是 2 的幂,索引全用移位,一次除法都不用);要改大小就改
`saolei.c` 顶部的 `BW/BH/NMINES`。
- `run_regress.ps1` 里还没有扫雷的自动测试项。
- **难度**:已按微软扫雷做成三档 —— `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 比对”的
一键回归。
+35 -7
View File
@@ -24,7 +24,7 @@
* 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_27FF 显存:40 列 x 30 行,每格 1 字节,共 1200 字节
* 0x0000_2000 - 0x0000_2FFF 显存:40 列 x 30 行,每格 2 字节,共 2400 字节
*
* 鼠标事件单位:1/8 格(按键一次 = 8 = 移动一格;PS/2 鼠标用原始计数)。
* 左键点击 -> 事件的 btn 位 0 置 1;右键(PS/2)-> 位 1。
@@ -69,13 +69,15 @@
#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(unsigned idx, unsigned val)
/* 显存按字节写(只能 sb,不能 lb —— 这颗核没有 lb/lbu)。每个格子占两个字节。 */
static inline void fb_write_cell(unsigned idx, unsigned glyph, unsigned color)
{
*(volatile unsigned char *)(FB_BASE + idx) = (unsigned char)(val & 0xFFu);
unsigned addr = FB_BASE + (idx << 1);
*(volatile unsigned char *)addr = (unsigned char)(glyph & 0x3Fu);
*(volatile unsigned char *)(addr + 1u) = (unsigned char)(color & 0x07u);
}
/* ---- 格子编码:1 字节 = [3:0] 字形 [6:4] 颜色 [7] 保留 ---- */
/* ---- 格子编码:两个字节 = 字形[5:0] + 颜色[2:0] ---- */
#define G_BLANK 0u /* 空 */
#define G_N1 1u /* 数字 1..8 = 1..8 */
#define G_COVER 9u /* 未揭开 */
@@ -86,6 +88,34 @@ static inline void fb_write(unsigned idx, unsigned val)
#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
@@ -95,8 +125,6 @@ static inline void fb_write(unsigned idx, unsigned val)
#define C_YELLOW 6u
#define C_GRAY 7u
#define CELL(g, c) ((unsigned)((g) | ((c) << 4)))
/* ---- 软件事件环形队列:中断服务程序(isr.c)生产,主循环(saolei.c)消费 ---- */
#define EVQ_SIZE 64u
#define EVQ_MASK (EVQ_SIZE - 1u)
+192 -85
View File
@@ -1,7 +1,7 @@
/* -----------------------------------------------------------------------------
* saolei.c —— 纯 C 扫雷(RV32IM 子集:只用这颗核实现了的指令)
*
* 显示:把每个格子写成显存里的 1 个字节(MMIO 0x2000 起)
* 显示:把每个格子写成显存里的 2 个字节(MMIO 0x2000 起)
* 输入:鼠标事件(中断驱动)——中断里搬到软件队列,主循环在下面处理
*
* 针对这颗 CPU 的硬约束(很重要,改代码时别踩):
@@ -13,19 +13,29 @@
* 所以本文件所有索引运算都用"2 的幂 + 移位/掩码",一次除法都不用。
* 4) 没有 fence/ebreak 等;中断返回是 uret(在 isr.c 里)。
*
* 棋盘:16 x 16,40 颗雷(16 是 2 的幂 => 索引全用移位,颜色/字形查表也不用除法)
* 棋盘:16 x 16,雷数可选 20/40/60(16 是 2 的幂 => 索引全用移位)
* 网格:40 列 x 30 行,棋盘居中放在 (行 7..22, 列 12..27)
* ---------------------------------------------------------------------------*/
#include "mmio.h"
#define BW 16u /* 棋盘宽(格) */
#define BH 16u /* 棋盘高(格) */
#define NCELLS (BW * BH) /* 256 */
#define NMINES 40u
#define BM_WORDS (NCELLS / 32u) /* 8 */
/* 三档难度完全照微软扫雷:初级 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 */
#define BOARD_C0 12u /* 棋盘左上角在网格里的列 */
#define BOARD_R0 7u /* 棋盘左上角在网格里的行 */
/* 当前难度下的棋盘参数(由 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
@@ -33,19 +43,22 @@
#define ST_LOSE 2u
/* ---- 棋盘状态:3 个位图,每个 8 个字(256 位) ---- */
static unsigned mine_bm[BM_WORDS];
static unsigned open_bm[BM_WORDS];
static unsigned flag_bm[BM_WORDS];
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]; /* 揭开用的队列 */
static unsigned bfs_q[NCELLS_MAX]; /* 揭开用的队列(按最大棋盘分配) */
/* ============================ 小工具 ============================ */
@@ -67,13 +80,13 @@ static void bclr(unsigned *bm, unsigned i)
static void draw_cell(unsigned gr, unsigned gc, unsigned glyph, unsigned color)
{
fb_write(gr * GRID_COLS + gc, CELL(glyph, 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(BOARD_R0 + r, BOARD_C0 + c, glyph, color);
draw_cell(g_board_r0 + r, g_board_c0 + c, glyph, color);
}
static unsigned num_color(unsigned n)
@@ -95,20 +108,20 @@ static unsigned num_color(unsigned n)
static unsigned in_board(void)
{
if (cur_r < BOARD_R0)
if (cur_r < g_board_r0)
return 0u;
if (cur_r >= (BOARD_R0 + BH))
if (cur_r >= (g_board_r0 + g_bh))
return 0u;
if (cur_c < BOARD_C0)
if (cur_c < g_board_c0)
return 0u;
if (cur_c >= (BOARD_C0 + BW))
if (cur_c >= (g_board_c0 + g_bw))
return 0u;
return 1u;
}
static unsigned cur_index(void)
{
return ((cur_r - BOARD_R0) << 4) + (cur_c - BOARD_C0); /* *BW(=16) 用移位 */
return ((cur_r - g_board_r0) * g_bw) + (cur_c - g_board_c0);
}
/* LCG 随机数:只用 mul/add/移位 */
@@ -118,6 +131,34 @@ static unsigned rnd_next(void)
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)
@@ -125,31 +166,31 @@ static void clear_screen(void)
unsigned i;
for (i = 0u; i < (GRID_COLS * GRID_ROWS); i = i + 1u)
{
fb_write(i, 0u);
fb_write_cell(i, G_BLANK, C_BLACK);
}
}
static void draw_frame(void)
{
unsigned gc, gr;
for (gc = (BOARD_C0 - 1u); gc <= (BOARD_C0 + BW); gc = gc + 1u)
for (gc = (g_board_c0 - 1u); gc <= (g_board_c0 + g_bw); gc = gc + 1u)
{
draw_cell(BOARD_R0 - 1u, gc, G_SOLID, C_CYAN);
draw_cell(BOARD_R0 + BH, gc, G_SOLID, C_CYAN);
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 = (BOARD_R0 - 1u); gr <= (BOARD_R0 + BH); gr = gr + 1u)
for (gr = (g_board_r0 - 1u); gr <= (g_board_r0 + g_bh); gr = gr + 1u)
{
draw_cell(gr, BOARD_C0 - 1u, G_SOLID, C_CYAN);
draw_cell(gr, BOARD_C0 + BW, G_SOLID, C_CYAN);
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 < BH; r = r + 1u)
for (r = 0u; r < g_bh; r = r + 1u)
{
for (c = 0u; c < BW; c = c + 1u)
for (c = 0u; c < g_bw; c = c + 1u)
{
draw_tile(r, c, G_COVER, C_GRAY);
}
@@ -158,7 +199,8 @@ static void draw_all_covered(void)
static void update_cursor_reg(void)
{
REG_W(REG_CURSOR) = (cur_c & 31u) | ((cur_r & 31u) << 8);
/* 列用 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)
@@ -170,11 +212,11 @@ static void set_bg(unsigned color)
static unsigned count_adj(unsigned i)
{
unsigned r = i >> 4, c = i & 15u, n = 0u, rr, cc;
unsigned r = IDX_R(i), c = IDX_C(i), n = 0u, rr, cc;
unsigned r0 = (r == 0u) ? 0u : (r - 1u);
unsigned r1 = (r + 1u >= BH) ? (BH - 1u) : (r + 1u);
unsigned r1 = (r + 1u >= g_bh) ? (g_bh - 1u) : (r + 1u);
unsigned c0 = (c == 0u) ? 0u : (c - 1u);
unsigned c1 = (c + 1u >= BW) ? (BW - 1u) : (c + 1u);
unsigned c1 = (c + 1u >= g_bw) ? (g_bw - 1u) : (c + 1u);
for (rr = r0; rr <= r1; rr = rr + 1u)
{
@@ -182,7 +224,7 @@ static unsigned count_adj(unsigned i)
{
if ((rr == r) && (cc == c))
continue;
n = n + bget(mine_bm, (rr << 4) + cc);
n = n + bget(mine_bm, (rr * g_bw) + cc);
}
}
return n;
@@ -191,17 +233,17 @@ static unsigned count_adj(unsigned i)
static void lose(unsigned hit)
{
unsigned k;
for (k = 0u; k < NCELLS; k = k + 1u)
for (k = 0u; k < g_ncells; k = k + 1u)
{
if (bget(mine_bm, k))
{
if (k == hit)
{
draw_tile(k >> 4, k & 15u, G_BOOM, C_RED);
draw_tile(IDX_R(k), IDX_C(k), G_BOOM, C_RED);
}
else if (!bget(flag_bm, k))
{
draw_tile(k >> 4, k & 15u, G_MINE, C_RED);
draw_tile(IDX_R(k), IDX_C(k), G_MINE, C_RED);
}
}
}
@@ -212,11 +254,11 @@ static void lose(unsigned hit)
static void win(void)
{
unsigned k;
for (k = 0u; k < NCELLS; k = k + 1u)
for (k = 0u; k < g_ncells; k = k + 1u)
{
if (bget(mine_bm, k))
{
draw_tile(k >> 4, k & 15u, G_FLAG, C_YELLOW);
draw_tile(IDX_R(k), IDX_C(k), G_FLAG, C_YELLOW);
}
}
REG_W(LED_ADDR) = 0xF00Du;
@@ -226,7 +268,7 @@ static void win(void)
static void reveal_at(unsigned i)
{
unsigned qh = 0u, qt = 0u, cur, n, r, c, rr, cc, ni;
unsigned r0, r1, c0, c1;
unsigned r0, r1, c0, c1, k;
if (bget(open_bm, i) || bget(flag_bm, i))
return;
@@ -236,8 +278,12 @@ static void reveal_at(unsigned 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)
{
@@ -250,27 +296,30 @@ static void reveal_at(unsigned i)
opened_cnt = opened_cnt + 1u;
n = count_adj(cur);
draw_tile(cur >> 4, cur & 15u, n, num_color(n));
draw_tile(IDX_R(cur), IDX_C(cur), n, num_color(n));
if (n != 0u)
continue;
/* 空白格:把周围 8 格入队(队列有界,满了就丢,不会越界) */
r = cur >> 4;
c = cur & 15u;
/* 空白格:把周围 8 格入队(已入过队的跳过,队列不会溢出) */
r = IDX_R(cur);
c = IDX_C(cur);
r0 = (r == 0u) ? 0u : (r - 1u);
r1 = (r + 1u >= BH) ? (BH - 1u) : (r + 1u);
r1 = (r + 1u >= g_bh) ? (g_bh - 1u) : (r + 1u);
c0 = (c == 0u) ? 0u : (c - 1u);
c1 = (c + 1u >= BW) ? (BW - 1u) : (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 << 4) + cc;
if (qt < NCELLS)
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;
}
@@ -278,21 +327,21 @@ static void reveal_at(unsigned i)
}
}
if (opened_cnt == (NCELLS - NMINES))
if (opened_cnt == (g_ncells - g_mines))
win();
}
static void place_mines(unsigned safe)
{
unsigned placed = 0u, i, r, c;
unsigned sr = safe >> 4;
unsigned sc = safe & 15u;
unsigned sr = IDX_R(safe);
unsigned sc = IDX_C(safe);
while (placed < NMINES)
while (placed < g_mines)
{
i = rnd_next() & 255u; /* NCELLS = 256,正好 2 的幂,不需要取模 */
r = i >> 4;
c = i & 15u;
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)))
@@ -332,8 +381,8 @@ static void toggle_flag(void)
if (bget(open_bm, i))
return;
r = cur_r - BOARD_R0;
c = cur_c - BOARD_C0;
r = cur_r - g_board_r0;
c = cur_c - g_board_c0;
if (bget(flag_bm, i))
{
@@ -347,7 +396,7 @@ static void toggle_flag(void)
flags_cnt = flags_cnt + 1u;
draw_tile(r, c, G_FLAG, C_YELLOW);
}
REG_W(LED_ADDR) = NMINES - flags_cnt;
REG_W(LED_ADDR) = g_mines - flags_cnt;
}
/* ============================ 鼠标事件 ============================ */
@@ -358,14 +407,14 @@ static void move_cursor(int dr, int dc)
int nr = (int)cur_r + dr;
int nc = (int)cur_c + dc;
if (nr < (int)(BOARD_R0 - 1u))
nr = (int)(BOARD_R0 - 1u);
if (nr > (int)(BOARD_R0 + BH))
nr = (int)(BOARD_R0 + BH);
if (nc < (int)(BOARD_C0 - 1u))
nc = (int)(BOARD_C0 - 1u);
if (nc > (int)(BOARD_C0 + BW))
nc = (int)(BOARD_C0 + BW);
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;
@@ -385,6 +434,11 @@ static unsigned g_exit; /* 关卡内请求返回 */
#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)
@@ -420,6 +474,24 @@ static void handle_event(unsigned ev)
{ /* 左键 */
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))
{
@@ -427,7 +499,7 @@ static void handle_event(unsigned ev)
REG_W(REG_LEVEL) = 1u;
}
}
else if ((cur_r == (BOARD_R0 - 1u)) && (cur_c == (BOARD_C0 - 1u)))
else if ((cur_r == (g_board_r0 - 1u)) && (cur_c == (g_board_c0 - 1u)))
{
g_exit = 1u; /* 关卡:点左上角黄格 = 返回上一层 */
}
@@ -490,24 +562,58 @@ static void handle_event(unsigned ev)
* ========================================================================== */
static void draw_start_screen(void)
{
unsigned i;
unsigned i, selected_color;
clear_screen();
/* 标题区:两排雷图案,纯图形(字模里没有字母;要显示文字就得扩字模,见 README) */
for (i = 0u; i < 6u; i = i + 1u)
{
draw_cell(6u, 15u + i * 2u, G_MINE, C_RED);
draw_cell(8u, 15u + i * 2u, G_MINE, C_RED);
}
/* 标题和难度菜单使用 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)
@@ -533,19 +639,19 @@ static void draw_start_screen(void)
* -> 点左上角黄格 = 返回:清 LEVEL、return -> isr1_entry 恢复现场并 uret
* -> 回到背景程序被打断的那条指令,继续等下一次"开始"。
* ========================================================================== */
#define RET_R (BOARD_R0 - 1u)
#define RET_C (BOARD_C0 - 1u)
/* 关卡里“返回上一层”的黄格 = 棋盘左上角外面一格(随难度尺寸移动) */
void level_main(void)
{
unsigned r;
/* ---- 关卡初始化 ---- */
for (r = 0u; r < BM_WORDS; r = r + 1u)
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;
@@ -554,17 +660,17 @@ void level_main(void)
g_exit = 0u;
acc_x = 0;
acc_y = 0;
cur_r = BOARD_R0 + 8u;
cur_c = BOARD_C0 + 8u;
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(RET_R, RET_C, G_BAR, C_YELLOW); /* 左上角黄格 = 返回 */
set_bg(C_BLUE);
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) = NMINES;
REG_W(REG_SEG) = NMINES; /* 数码管显示剩余雷数 */
REG_W(LED_ADDR) = g_mines;
REG_W(REG_SEG) = g_mines; /* 数码管显示剩余雷数 */
/* ---- 关卡主循环 ---- */
while (g_exit == 0u)
@@ -595,7 +701,7 @@ void level_main(void)
}
else
{
set_bg(C_BLUE);
set_bg(C_BLACK);
}
}
}
@@ -615,6 +721,7 @@ 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; /* 清关卡软中断 */
+7 -2
View File
@@ -58,6 +58,11 @@ $OBJDUMP = $Prefix + 'objdump'
$OBJCOPY = $Prefix + 'objcopy'
$SIZE = $Prefix + 'size'
$Python = 'py'
if (-not (Get-Command $Python -ErrorAction SilentlyContinue)) {
$Python = 'python'
}
Write-Output "工具链前缀: $Prefix"
# ---------------------------------------------------------------- 编译参数
@@ -139,7 +144,7 @@ Write-Output "反汇编清单: $lst"
$isaOk = $true
if (-not $SkipIsaCheck) {
Write-Output ""
& py (Join-Path $PSScriptRoot 'check_isa.py') $lst
& $Python (Join-Path $PSScriptRoot 'check_isa.py') $lst
if ($LASTEXITCODE -ne 0) { $isaOk = $false }
}
@@ -149,7 +154,7 @@ $hex = Join-Path $build 'saolei.hex'
$vh = Join-Path $root '..\pipelined-cpu\srcs\board\cpu21_saolei_rom.vh'
& $OBJCOPY -O binary $elf $bin
& py (Join-Path $PSScriptRoot 'elf2hex.py') $bin $hex $RomWords $vh
& $Python (Join-Path $PSScriptRoot 'elf2hex.py') $bin $hex $RomWords $vh
# ---------------------------------------------------------------- 汇总
Write-Output ""
+15
View File
@@ -40,6 +40,21 @@ def main():
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))
+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()