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:
@@ -4,6 +4,7 @@ pipelined-cpu/*
|
|||||||
!pipelined-cpu/tools/
|
!pipelined-cpu/tools/
|
||||||
!pipelined-cpu/programs/
|
!pipelined-cpu/programs/
|
||||||
!pipelined-cpu/create_project.tcl
|
!pipelined-cpu/create_project.tcl
|
||||||
|
!pipelined-cpu/create_project_saolei.tcl
|
||||||
|
|
||||||
# 本地临时目录(仿真脚本、日志、中间文件)
|
# 本地临时目录(仿真脚本、日志、中间文件)
|
||||||
_tmp/
|
_tmp/
|
||||||
|
|||||||
@@ -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,183 @@
|
|||||||
|
`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 1200\n");
|
||||||
|
for (w2 = 0; w2 < 300; 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
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# 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 不按键,只重画一遍当前画面
|
||||||
|
# help 键位说明
|
||||||
|
#
|
||||||
|
# 原理: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]]
|
||||||
|
|
||||||
|
# 按键字符 -> 人类可读的动作(只用于回显)
|
||||||
|
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
|
||||||
|
if {![file exists "now.txt"]} {
|
||||||
|
puts " (now.txt not ready yet - run a bit first)"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if {[catch { exec py $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 help this text quit exit"
|
||||||
|
puts " PNG screenshot: py \"$::fb2png\" now.txt shot.png"
|
||||||
|
puts " ---------------------------------------------------------------------------"
|
||||||
|
}
|
||||||
|
|
||||||
|
# one step: send the key -> advance simulation -> 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
|
||||||
|
}
|
||||||
|
render
|
||||||
|
}
|
||||||
|
|
||||||
|
proc 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
|
||||||
+68
-20
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
用法:
|
用法:
|
||||||
py saolei/tools/fb2png.py <dump.txt> [输出.png]
|
py saolei/tools/fb2png.py <dump.txt> [输出.png]
|
||||||
|
py saolei/tools/fb2png.py --term <dump.txt> # 只在终端打印彩色棋盘(交互玩扫雷用)
|
||||||
|
|
||||||
dump 格式(tb_saolei.v 里的 dump_fb 任务生成):
|
dump 格式(tb_saolei.v 里的 dump_fb 任务生成):
|
||||||
CURSOR <row> <col>
|
CURSOR <row> <col>
|
||||||
@@ -101,15 +102,77 @@ def parse_dump(path):
|
|||||||
return cur, ctrl, fb
|
return cur, ctrl, fb
|
||||||
|
|
||||||
|
|
||||||
|
def cell_char(g):
|
||||||
|
"""字形号 -> 一个可见字符(字形 1..8 就是数字 1..8)。"""
|
||||||
|
if g in GLYPH_CH:
|
||||||
|
return GLYPH_CH[g]
|
||||||
|
if 1 <= g <= 8:
|
||||||
|
return chr(ord('0') + g)
|
||||||
|
return ' '
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
cell = fb[y * COLS + x]
|
||||||
|
g, c = cell & 0xF, (cell >> 4) & 0x7
|
||||||
|
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=爆炸 ?=问号 1..8=数字 空格=已揭开的空地;反色格 = 光标\n")
|
||||||
|
sys.stdout.write("".join(out))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if len(sys.argv) < 2:
|
args = list(sys.argv[1:])
|
||||||
|
term_only = "--term" in args
|
||||||
|
if term_only:
|
||||||
|
args.remove("--term")
|
||||||
|
if not args:
|
||||||
raise SystemExit(__doc__)
|
raise SystemExit(__doc__)
|
||||||
src = sys.argv[1]
|
src = args[0]
|
||||||
dst = sys.argv[2] if len(sys.argv) > 2 else src.rsplit(".", 1)[0] + ".png"
|
dst = args[1] if len(args) > 1 else src.rsplit(".", 1)[0] + ".png"
|
||||||
cur, ctrl, fb = parse_dump(src)
|
cur, ctrl, fb = parse_dump(src)
|
||||||
|
|
||||||
bg = (ctrl >> 4) & 0x7
|
bg = (ctrl >> 4) & 0x7
|
||||||
cursor_box = bool(ctrl & 1) and cur[0] is not None
|
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"
|
print("光标 = (%s, %s) 光标可见 = %s 背景色 = %d"
|
||||||
% (cur[0], cur[1], bool(ctrl & 1), bg))
|
% (cur[0], cur[1], bool(ctrl & 1), bg))
|
||||||
|
|
||||||
@@ -135,23 +198,8 @@ def main():
|
|||||||
write_png(dst, COLS * CELL_PX, ROWS * CELL_PX, rows)
|
write_png(dst, COLS * CELL_PX, ROWS * CELL_PX, rows)
|
||||||
print("已生成 %s(%dx%d)" % (dst, COLS * CELL_PX, ROWS * CELL_PX))
|
print("已生成 %s(%dx%d)" % (dst, COLS * CELL_PX, ROWS * CELL_PX))
|
||||||
|
|
||||||
# ---- 终端 ASCII 图 ----
|
# ---- 终端 ASCII 图(彩色版;PNG 已经写好了,这里只是方便快速看一眼)----
|
||||||
print("+" + "-" * COLS + "+")
|
draw_term(fb, cur, cursor_box, dump_note(src))
|
||||||
for y in range(ROWS):
|
|
||||||
line = ""
|
|
||||||
for x in range(COLS):
|
|
||||||
cell = fb[y * COLS + x]
|
|
||||||
g, c = cell & 0xF, (cell >> 4) & 0x7
|
|
||||||
if cursor_box and (x, y) == cur:
|
|
||||||
line += "["
|
|
||||||
elif g in GLYPH_CH:
|
|
||||||
line += GLYPH_CH[g]
|
|
||||||
else:
|
|
||||||
line += chr(ord('0') + g) # 字形 1..8 就是数字 1..8
|
|
||||||
line = line.replace("[", "[]" * 1) # 光标只提示左边界,避免打乱列数
|
|
||||||
print("|" + (line + " " * COLS)[:COLS] + "|")
|
|
||||||
print("+" + "-" * COLS + "+")
|
|
||||||
print("图例: #=未揭开 P=旗 *=雷 X=爆炸 ?=问号 -=横线 ==实心 1..8=数字")
|
|
||||||
|
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user