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
This commit is contained in:
2026-09-19 19:54:56 +08:00
parent 0908580e46
commit 132c9202f6
5 changed files with 515 additions and 20 deletions
+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 要一起改。"