Files
risc-v-cpu/pipelined-cpu/srcs/tb/tb_saolei_play.v
T
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

202 lines
7.5 KiB
Verilog
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
`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