`timescale 1ns / 1ps // ============================================================================= // tb_saolei —— 没有 VGA 显示器时的"扫雷调试台" // // 做法:跑真实的板级顶层 cpu21_board_top(宏 SAOLEI_ROM = 扫雷程序),用 5 向按键 // 模拟鼠标(板上 BTNC = 左键、BTNU/D/L/R = 上下左右,R15=SW3 = 插旗模式), // 定期把显存 1200 字节 dump 成文本;再由 saolei/tools/fb2png.py 按 cpu21_vga.v // 里的 8x8 字库与 3 位调色板还原成 PNG —— 等价于给 VGA 画面"截图"。 // // 编译(注意两个 -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.v // xelab -d SAOLEI_ROM -top tb_saolei -snapshot saolei_sim // xsim saolei_sim -tclbatch run_all.tcl // // 说明:为了在仿真里按键快一点,用 defparam 把 cpu21_pointer 的去抖采样周期 // 从 50000 个 CPU 周期(1 ms @50 MHz)压到 50(1 us),这样每次"按键"只要几微秒 // 就能产生一个事件;上板时那个参数仍然是 50000,行为不变。 // ============================================================================= module tb_saolei; // ---- 板级顶层端口 ---- 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; // 100 MHz 参考时钟 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) ); // (按键去抖已通过上面的 POINTER_SAMPLE_DIV(50) 参数加速,不用 defparam) // ---------------------------------------------------------------- 按键 // which: 0=go(左键) 1=btnu 2=btnd 3=btnl 4=btnr task press; input integer which; input integer us; // 按住时长(微秒) begin case (which) 0: go = 1'b1; 1: btnu = 1'b1; 2: btnd = 1'b1; 3: btnl = 1'b1; 4: btnr = 1'b1; endcase #(us * 1000); case (which) 0: go = 1'b0; 1: btnu = 1'b0; 2: btnd = 1'b0; 3: btnl = 1'b0; 4: btnr = 1'b0; endcase #(200 * 1000); // 松开后留 200 us 让 CPU 消费事件 end endtask task click; // 左键点一下 begin press(0, 50); end endtask // ---------------------------------------------------------------- 显存 dump integer fh, w, b; integer cursor_row, cursor_col; // ---- 统计:MMIO 显存写脉冲数 + 当前非零格数 ---- integer fb_we_cnt = 0; always @(posedge dut.clk_cpu) if (dut.fb_we) 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 时统计) // ---- PC 热点直方图:确定 CPU 此刻卡在哪个循环 ---- integer pc_hist[0:255]; integer pi; initial for (pi = 0; pi < 256; pi = pi + 1) pc_hist[pi] = 0; always @(posedge dut.clk_cpu) pc_hist[dut.u_cpu.pc_q[9:2]] = pc_hist[dut.u_cpu.pc_q[9:2]] + 1; // ---- 探针:控制流日志 ---- // 1) 记录所有非顺序跳转(1 ms 之后、最多 300 条),用来复盘"跑飞"的路径; // 2) 单独标出跳进 ISR 段之间空洞(0x194-0x1FF / 0x218-0x2FF)的时刻。 reg [31:0] pc_prev; integer jmp_log; initial begin pc_prev = 32'hFFFF_FFFF; jmp_log = 0; end always @(posedge dut.clk_cpu) begin pc_prev <= dut.u_cpu.pc_q; if (dut.u_cpu.pc_q != (pc_prev + 32'd4)) begin if ($time > 1000000 && jmp_log < 300) begin $display("[jmp] %0t ns: 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 task dump_pc; integer i1, i2, best, besti; begin $display(" [pc] 热点(pc, 命中数):"); for (i1 = 0; i1 < 3; i1 = i1 + 1) begin best = -1; besti = -1; for (i2 = 0; i2 < 256; i2 = i2 + 1) if (pc_hist[i2] > best) begin best = pc_hist[i2]; besti = i2; end if (besti >= 0 && best > 0) begin $display(" pc=0x%03h count=%0d", besti << 2, best); pc_hist[besti] = 0; end end end endtask // ---- RAM 窗口(核对变量地址映射)---- task dump_ramwin; integer i3; begin $display(" [ram] 0x00..0x3F: %08h %08h %08h %08h %08h %08h %08h %08h", dut.ram[0], dut.ram[1], dut.ram[2], dut.ram[3], dut.ram[4], dut.ram[5], dut.ram[6], dut.ram[7]); $display(" [ram] 0x100..0x11F: %08h %08h %08h %08h %08h %08h %08h %08h", dut.ram[32'h0100 >> 2], dut.ram[32'h0104 >> 2], dut.ram[32'h0108 >> 2], dut.ram[32'h010C >> 2], dut.ram[32'h0110 >> 2], dut.ram[32'h0114 >> 2], dut.ram[32'h0118 >> 2], dut.ram[32'h011C >> 2]); end endtask // g_exit=0x04 g_in_level=0x08 acc_y=0x40C acc_x=0x410 cur_c=0x414 // cur_r=0x418 g_first_done=0x420 g_state=0x424 // g_evq_lost=0x490 g_evq_tail=0x494 g_evq_head=0x498 task dump_vars; begin $display(" [vars] exit=%0d in_level=%0d cur=(r%0d,c%0d) acc=(%0d,%0d) first=%0d state=%0d lost=%0d evq(h=%0d,t=%0d)", dut.ram[32'h0004 >> 2], dut.ram[32'h0008 >> 2], dut.ram[32'h0418 >> 2], dut.ram[32'h0414 >> 2], dut.ram[32'h0410 >> 2], dut.ram[32'h040C >> 2], dut.ram[32'h0420 >> 2], dut.ram[32'h0424 >> 2], dut.ram[32'h0490 >> 2], dut.ram[32'h0498 >> 2], dut.ram[32'h0494 >> 2]); end endtask task dump_fb; input [1023:0] fname; integer k2, b2; begin nz_cnt = 0; for (k2 = 0; k2 < 300; k2 = k2 + 1) for (b2 = 0; b2 < 4; b2 = b2 + 1) if (dut.fb[k2][b2*8 +: 8] != 8'h00) nz_cnt = nz_cnt + 1; cursor_row = dut.u_vga.cursor_i[12:8]; cursor_col = dut.u_vga.cursor_i[4:0]; fh = $fopen(fname, "w"); $fwrite(fh, "# tb_saolei framebuffer dump\n"); $fwrite(fh, "TIME %0t\n", $time); $fwrite(fh, "CURSOR %0d %0d\n", cursor_row, cursor_col); $fwrite(fh, "CTRL 0x%02X\n", dut.u_vga.ctrl_i[7:0]); $fwrite(fh, "STATS fb_we=%0d nonzero=%0d\n", fb_we_cnt, nz_cnt); $fwrite(fh, "FB 1200\n"); for (w = 0; w < 300; w = w + 1) for (b = 0; b < 4; b = b + 1) $fwrite(fh, "%02X\n", dut.fb[w][b*8 +: 8]); $fclose(fh); $display("[tb_saolei] %0t ns dump -> %0s (cursor=%0d,%0d fb_we=%0d nonzero=%0d)", $time, fname, cursor_row, cursor_col, fb_we_cnt, nz_cnt); dump_vars; dump_pc; dump_ramwin; end 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 $display("[tb_saolei] === start (SAOLEI_ROM):完整一局 ==="); #2000; rst_n = 1'b1; // 1) 开始界面 #(2 * 1000000); dump_fb("fb_00_start.txt"); // 2) 点 START(光标初始就停在按钮上)-> 写 LEVEL -> IRQ1 进关卡 click_t; #(2 * 1000000); dump_fb("fb_01_level.txt"); $display(" [game] 进关卡:in_level=%0d state=%0d", dut.ram[32'h0008 >> 2], dut.ram[32'h0424 >> 2]); // 3) 关卡内点盘面中心一格:放雷 + 首次展开(第一下必安全) click; #(3 * 1000000); 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]); // 4) 蛇形扫完全盘:揭开全部 216 个安全格 + 给 40 颗雷插旗 -> 目标通关 sweep_board; #(3 * 1000000); dump_fb("fb_13_after_sweep.txt"); count_open; $display(" [game] 扫完后:已揭开 %0d / 216, 旗=%0d, state=%0d, 丢事件=%0d", open_n, dut.ram[32'h0428 >> 2], dut.ram[32'h0424 >> 2], dut.ram[32'h0490 >> 2]); // 5) 通关后再点一下 = 返回背景(回到开始界面) click_t; #(3 * 1000000); dump_fb("fb_14_back_to_start.txt"); $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; end endmodule