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
+183
View File
@@ -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