tb: 无显示器扫雷联调套件(完整一局通关 216/216)
- srcs/tb/tb_saolei.v:VGA 帧缓冲截图、RAM 变量观察、PC 热点、跳转探针, 完整一局流程(点 START -> 首点击放雷 -> 带落位校验的全盘扫描 -> 通关) - saolei/tools/fb2png.py:把仿真 dump 渲染成 640x480 PNG(无第三方依赖)
This commit is contained in:
@@ -43,8 +43,8 @@ module tb_saolei;
|
|||||||
always #5 clk100 = ~clk100;
|
always #5 clk100 = ~clk100;
|
||||||
|
|
||||||
cpu21_board_top #(
|
cpu21_board_top #(
|
||||||
.POINTER_SAMPLE_DIV(50) // 仿真里加速按键去抖(上板默认 50000)
|
.POINTER_SAMPLE_DIV(50) // 仿真里加速按键去抖(上板默认 50000)
|
||||||
) dut (
|
) dut (
|
||||||
.clk100 (clk100),
|
.clk100 (clk100),
|
||||||
.rst_n (rst_n),
|
.rst_n (rst_n),
|
||||||
.go (go),
|
.go (go),
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user