tb: 无显示器扫雷联调套件(完整一局通关 216/216)

- srcs/tb/tb_saolei.v:VGA 帧缓冲截图、RAM 变量观察、PC 热点、跳转探针,
  完整一局流程(点 START -> 首点击放雷 -> 带落位校验的全盘扫描 -> 通关)
- saolei/tools/fb2png.py:把仿真 dump 渲染成 640x480 PNG(无第三方依赖)
This commit is contained in:
2026-09-19 19:43:07 +08:00
parent 47d2f22d07
commit b8c62c2726
+207 -40
View File
@@ -121,25 +121,59 @@ module tb_saolei;
if (dut.fb_we) if (dut.fb_we)
fb_we_cnt = fb_we_cnt + 1; fb_we_cnt = fb_we_cnt + 1;
// ---- 鼠标事件链路三个关键节点 ----
// ptr_ev : cpu21_pointer 产生事件(ev_valid_o 高的周期数)
// irq_hi : 鼠标中断线 irq_vec[0] 为高的周期数
// irq_rise: 该线的上升沿次数(CPU 按上升沿响应中断)
integer ptr_ev = 0, irq_hi = 0, irq_rise = 0;
reg irq_prev_q;
initial irq_prev_q = 1'b0;
always @(posedge dut.clk_cpu) begin
if (dut.g_btn.u_ptr.ev_valid_o) ptr_ev = ptr_ev + 1;
if (dut.irq_vec[0]) irq_hi = irq_hi + 1;
irq_prev_q <= dut.irq_vec[0];
if (dut.irq_vec[0] && !irq_prev_q) irq_rise = irq_rise + 1;
end
// 每毫秒打一行,观察事件链路
integer tick_ms;
initial
for (tick_ms = 1; tick_ms <= 20; tick_ms = tick_ms + 1) begin
#1000000;
$display("[mouse] %0t ns: ptr_ev=%0d irq_hi=%0d irq_rise=%0d", $time, ptr_ev, irq_hi, irq_rise);
end
integer nz_cnt = 0; // 当前非零格数(dump 时统计) integer nz_cnt = 0; // 当前非零格数(dump 时统计)
// ---- PC 热点直方图:确定 CPU 此刻卡在哪个循环 ---- // ---- PC 热点直方图:确定 CPU 此刻卡在哪个循环 ----
integer pc_hist[0:255]; integer pc_hist[0:255];
integer pi; integer pi;
initial for (pi = 0; pi < 256; pi = pi + 1) pc_hist[pi] = 0; initial
for (pi = 0; pi < 256; pi = pi + 1)
pc_hist[pi] = 0;
always @(posedge dut.clk_cpu) always @(posedge dut.clk_cpu)
pc_hist[dut.u_cpu.pc_q[9:2]] = pc_hist[dut.u_cpu.pc_q[9:2]] + 1; pc_hist[dut.u_cpu.pc_q[9:2]] = pc_hist[dut.u_cpu.pc_q[9:2]] + 1;
// ---- 探针:PC 什么时候"跳进" ISR 段之间的空洞(0x194-0x1FF / 0x218-0x2FF)---- // ---- 探针:控制流日志 ----
// 这些空洞在 ROM 里是 0x00000000,在本 CPU 上会被当成自跳指令,PC 掉进去出不来。 // 1) 记录所有非顺序跳转(1 ms 之后、最多 300 条),用来复盘"跑飞"的路径;
// 2) 单独标出跳进 ISR 段之间空洞(0x194-0x1FF / 0x218-0x2FF)的时刻。
reg [31:0] pc_prev; reg [31:0] pc_prev;
initial pc_prev = 32'hFFFF_FFFF; integer jmp_log;
initial begin
pc_prev = 32'hFFFF_FFFF;
jmp_log = 0;
end
always @(posedge dut.clk_cpu) begin always @(posedge dut.clk_cpu) begin
pc_prev <= dut.u_cpu.pc_q; pc_prev <= dut.u_cpu.pc_q;
if (dut.u_cpu.pc_q != (pc_prev + 32'd4) && if (dut.u_cpu.pc_q != (pc_prev + 32'd4)) begin
((dut.u_cpu.pc_q >= 32'h0000_0194 && dut.u_cpu.pc_q < 32'h0000_0200) || if ($time > 1000000 && jmp_log < 300) begin
(dut.u_cpu.pc_q >= 32'h0000_0218 && dut.u_cpu.pc_q < 32'h0000_0300))) $display("[jmp] %0t ns: 0x%03h -> 0x%03h", $time, pc_prev, dut.u_cpu.pc_q);
$display("[gap] %0t ns: PC 0x%03h -> 0x%03h (掉进空洞)", $time, pc_prev, dut.u_cpu.pc_q); jmp_log = jmp_log + 1;
end
if ((dut.u_cpu.pc_q >= 32'h0000_0194 && dut.u_cpu.pc_q < 32'h0000_0200) ||
(dut.u_cpu.pc_q >= 32'h0000_0218 && dut.u_cpu.pc_q < 32'h0000_0300))
$display("[gap] %0t ns: PC 0x%03h -> 0x%03h (跳进空洞)", $time, pc_prev, dut.u_cpu.pc_q);
end
end end
task dump_pc; task dump_pc;
@@ -222,52 +256,185 @@ module tb_saolei;
end end
endtask endtask
// ---------------------------------------------------------------- 完整一局用的工具
// 从 RAM 的位图里取一位(mine_bm @0x470 / open_bm @0x450,字内小端)
function integer bitof;
input integer base;
input integer idx;
begin
bitof = (dut.ram[(base >> 2) + (idx >> 5)] >> (idx & 31)) & 1;
end
endfunction
// 已揭开格数(直接数 open_bm 的 1),目标 = 256 - 40 = 216
integer open_n;
task count_open;
integer k3;
begin
open_n = 0;
for (k3 = 0; k3 < 256; k3 = k3 + 1)
open_n = open_n + bitof(32'h0000_0450, k3);
end
endtask
// 快速按键(按住/松开时长都可控):去抖周期已由 POINTER_SAMPLE_DIV=50 缩短
task press_t;
input integer which;
input integer hold_us;
input integer gap_us;
begin
case (which)
0: go = 1'b1;
1: btnu = 1'b1;
2: btnd = 1'b1;
3: btnl = 1'b1;
default: btnr = 1'b1;
endcase
#(hold_us * 1000);
case (which)
0: go = 1'b0;
1: btnu = 1'b0;
2: btnd = 1'b0;
3: btnl = 1'b0;
default: btnr = 1'b0;
endcase
#(gap_us * 1000);
end
endtask
task click_t;
begin
press_t(0, 30, 150);
end
endtask
// 把光标移到盘面格 (tr, tc):每次按键后等软件把事件消化掉再回读位置,
// 丢了/多了事件都能自动纠正。盘面格 (0,0) 对应网格 (BOARD_R0, BOARD_C0) = (7, 12)。
task move_to_cell;
input integer tr;
input integer tc;
integer cr, cc2;
begin
cr = dut.ram[32'h0418 >> 2];
cc2 = dut.ram[32'h0414 >> 2];
while (cr > (7 + tr)) begin
press_t(1, 20, 120);
cr = dut.ram[32'h0418 >> 2];
end
while (cr < (7 + tr)) begin
press_t(2, 20, 120);
cr = dut.ram[32'h0418 >> 2];
end
while (cc2 > (12 + tc)) begin
press_t(3, 20, 120);
cc2 = dut.ram[32'h0414 >> 2];
end
while (cc2 < (12 + tc)) begin
press_t(4, 20, 120);
cc2 = dut.ram[32'h0414 >> 2];
end
end
endtask
// 落位 + 校验:没到位就重试(最多 6 次),彻底避免“以为到了、实际差一格”去点错格
task goto_cell;
input integer tr;
input integer tc;
integer tries;
begin
tries = 0;
while (((dut.ram[32'h0418 >> 2] != (7 + tr)) || (dut.ram[32'h0414 >> 2] != (12 + tc))) && (tries < 6)) begin
move_to_cell(tr, tc);
#(300 * 1000);
tries = tries + 1;
end
if (tries >= 6)
$display(" [game] !!! 定位 (%0d,%0d) 失败,实际 cur=(%0d,%0d)", tr, tc,
dut.ram[32'h0418 >> 2], dut.ram[32'h0414 >> 2]);
end
endtask
// 扫完全盘:安全格揭开、雷格插旗;雷区位置直接从 RAM 里的 mine_bm 读
task sweep_board;
integer rr, cc, idx;
begin : sweep_blk
for (rr = 0; rr < 16; rr = rr + 1) begin
for (cc = 0; cc < 16; cc = cc + 1) begin
idx = rr * 16 + cc;
goto_cell(rr, cc);
if (bitof(32'h0000_0470, idx) != 0) begin
sw_flag = 1'b1;
#(30 * 1000);
click_t;
sw_flag = 1'b0;
#(30 * 1000);
end
else
click_t;
// 中途退出关卡就立即报警并收手
if (dut.ram[32'h0008 >> 2] == 0) begin
$display(" [game] *** 在盘面 (%0d,%0d) 处退出了关卡!cur=(%0d,%0d) state=%0d",
rr, cc, dut.ram[32'h0418 >> 2], dut.ram[32'h0414 >> 2],
dut.ram[32'h0424 >> 2]);
disable sweep_blk;
end
// 赢了/输了也收手
if (dut.ram[32'h0424 >> 2] != 0)
disable sweep_blk;
end
count_open;
$display(" [game] 第 %0d 行扫完:已揭开 %0d / 216, state=%0d, 旗=%0d, cur=(%0d,%0d)",
rr, open_n, dut.ram[32'h0424 >> 2], dut.ram[32'h0428 >> 2],
dut.ram[32'h0418 >> 2], dut.ram[32'h0414 >> 2]);
if (rr == 3)
dump_fb("fb_10_sweep_a.txt");
if (rr == 7)
dump_fb("fb_11_sweep_b.txt");
if (rr == 11)
dump_fb("fb_12_sweep_c.txt");
end
end
endtask
// ---------------------------------------------------------------- 主流程 // ---------------------------------------------------------------- 主流程
initial begin initial begin
$display("[tb_saolei] === start (SAOLEI_ROM) ==="); $display("[tb_saolei] === start (SAOLEI_ROM):完整一局 ===");
#2000; #2000;
rst_n = 1'b1; rst_n = 1'b1;
// 1) 背景开始界面:光标初始就停在 START 按钮上(行 20,列 17..22) // 1) 开始界面
#(2 * 1000000); #(2 * 1000000);
dump_fb("fb_00_start.txt"); dump_fb("fb_00_start.txt");
// 2) 验证移动的方向与步长:下 2 格、右 2 格,再原路回位 // 2) 点 START(光标初始就停在按钮上)-> 写 LEVEL -> IRQ1 进关卡
repeat (2) press(2, 200); click_t;
repeat (2) press(4, 200);
dump_fb("fb_01_moved.txt");
repeat (2) press(1, 200);
repeat (2) press(3, 200);
dump_fb("fb_02_back.txt");
// 3) 点"开始" -> 写 LEVEL -> IRQ1 进关卡
click;
#(2 * 1000000); #(2 * 1000000);
dump_fb("fb_03_level.txt"); dump_fb("fb_01_level.txt");
$display(" [game] 进关卡:in_level=%0d state=%0d", dut.ram[32'h0008 >> 2], dut.ram[32'h0424 >> 2]);
// 4) 关卡内:往下 2 格、往右 2 格,再点一下揭开 // 3) 关卡内点盘面中心一格:放雷 + 首次展开(第一下必安全)
repeat (2) press(2, 200);
repeat (2) press(4, 200);
dump_fb("fb_04_cursor.txt");
click; click;
#(2 * 1000000); #(3 * 1000000);
dump_fb("fb_05_firstclick.txt"); dump_fb("fb_02_firstclick.txt");
count_open;
$display(" [game] 首次点击后:first=%0d 已揭开 %0d / 216 state=%0d",
dut.ram[32'h0420 >> 2], open_n, dut.ram[32'h0424 >> 2]);
// 5) 插旗(SW3 = 插旗模式 + 左键) // 4) 蛇形扫完全盘:揭开全部 216 个安全格 + 给 40 颗雷插旗 -> 目标通关
sw_flag = 1'b1; sweep_board;
#(100 * 1000); #(3 * 1000000);
click; dump_fb("fb_13_after_sweep.txt");
#(2 * 1000000); count_open;
sw_flag = 1'b0; $display(" [game] 扫完后:已揭开 %0d / 216, 旗=%0d, state=%0d, 丢事件=%0d",
dump_fb("fb_06_flag.txt"); open_n, dut.ram[32'h0428 >> 2], dut.ram[32'h0424 >> 2], dut.ram[32'h0490 >> 2]);
// 6) 往右一格再揭开 // 5) 通关后再点一下 = 返回背景(回到开始界面)
press(4, 200); click_t;
click; #(3 * 1000000);
#(2 * 1000000); dump_fb("fb_14_back_to_start.txt");
dump_fb("fb_07_click2.txt");
$display("[tb_saolei] === done, LED=%04h led_we=%b ===", led, led_we); $display(" [game] === 结束:state=%0d(1=WIN 2=LOSE)已揭开 %0d/216 旗=%0d 丢事件=%0d ===",
dut.ram[32'h0424 >> 2], open_n, dut.ram[32'h0428 >> 2], dut.ram[32'h0490 >> 2]);
$finish; $finish;
end end