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 已修好
202 lines
7.5 KiB
Verilog
202 lines
7.5 KiB
Verilog
`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
|