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 已修好
465 lines
18 KiB
Verilog
465 lines
18 KiB
Verilog
`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
|
||
|
||
// 最新版(显存每格 2 字节、难度三档不同尺寸)变量地址:
|
||
// g_exit=0x04 g_in_level=0x08 acc_y=0x78C acc_x=0x790 cur_c=0x794
|
||
// cur_r=0x798 g_difficulty=0x7A0 g_mines=0x7A4 g_first_done=0x7A8
|
||
// g_state=0x7AC flags_cnt=0x7B0 opened_cnt=0x7B4
|
||
// flag_bm=0x7F4 open_bm=0x830 mine_bm=0x86C
|
||
// g_ncells=0x8B4 g_evq_lost=0x8C0 g_evq_tail=0x8C4 g_evq_head=0x8C8
|
||
task dump_vars;
|
||
begin
|
||
$display(" [vars] exit=%0d in_level=%0d cur=(r%0d,c%0d) acc=(%0d,%0d) diff=%0d mines=%0d first=%0d state=%0d flags=%0d opened=%0d lost=%0d evq(h=%0d,t=%0d)",
|
||
dut.ram[32'h0004 >> 2], dut.ram[32'h0008 >> 2],
|
||
dut.ram[32'h0798 >> 2], dut.ram[32'h0794 >> 2],
|
||
dut.ram[32'h0790 >> 2], dut.ram[32'h078C >> 2],
|
||
dut.ram[32'h07A0 >> 2], dut.ram[32'h07A4 >> 2],
|
||
dut.ram[32'h07A8 >> 2], dut.ram[32'h07AC >> 2],
|
||
dut.ram[32'h07B0 >> 2], dut.ram[32'h07B4 >> 2],
|
||
dut.ram[32'h08C0 >> 2],
|
||
dut.ram[32'h08C8 >> 2], dut.ram[32'h08C4 >> 2]);
|
||
end
|
||
endtask
|
||
|
||
task dump_fb;
|
||
input [1023:0] fname;
|
||
integer k2, b2;
|
||
begin
|
||
nz_cnt = 0;
|
||
for (k2 = 0; k2 < 600; 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 2400\n");
|
||
for (w = 0; w < 600; 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 @0x86C / open_bm @0x830 / flag_bm @0x7F4)
|
||
function integer bitof;
|
||
input integer base;
|
||
input integer idx;
|
||
begin
|
||
bitof = (dut.ram[(base >> 2) + (idx >> 5)] >> (idx & 31)) & 1;
|
||
end
|
||
endfunction
|
||
|
||
// 已揭开格数:直接读程序里的 opened_cnt(0x7B4)
|
||
integer open_n;
|
||
task count_open;
|
||
begin
|
||
open_n = dut.ram[32'h07B4 >> 2];
|
||
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_086C, 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'h0798 >> 2], dut.ram[32'h0794 >> 2],
|
||
dut.ram[32'h07AC >> 2]);
|
||
disable sweep_blk;
|
||
end
|
||
// 赢了/输了也收手
|
||
if (dut.ram[32'h07AC >> 2] != 0)
|
||
disable sweep_blk;
|
||
end
|
||
count_open;
|
||
$display(" [game] 第 %0d 行扫完:已揭开 %0d / %0d, state=%0d, 旗=%0d, cur=(%0d,%0d)",
|
||
rr, open_n,
|
||
(dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
|
||
dut.ram[32'h07AC >> 2], dut.ram[32'h07B0 >> 2],
|
||
dut.ram[32'h0798 >> 2], dut.ram[32'h0794 >> 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'h07AC >> 2]);
|
||
|
||
// 3) 关卡内点盘面中心一格:放雷 + 首次展开(第一下必安全)
|
||
click;
|
||
#(3 * 1000000);
|
||
dump_fb("fb_02_firstclick.txt");
|
||
count_open;
|
||
$display(" [game] 首次点击后:first=%0d 已揭开 %0d / %0d state=%0d diff=%0d",
|
||
dut.ram[32'h07A8 >> 2], open_n,
|
||
(dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
|
||
dut.ram[32'h07AC >> 2], dut.ram[32'h07A0 >> 2]);
|
||
|
||
// 4) 蛇形扫完全盘:揭开全部 216 个安全格 + 给 40 颗雷插旗 -> 目标通关
|
||
sweep_board;
|
||
#(3 * 1000000);
|
||
dump_fb("fb_13_after_sweep.txt");
|
||
count_open;
|
||
$display(" [game] 扫完后:已揭开 %0d / %0d, 旗=%0d, state=%0d, 丢事件=%0d",
|
||
open_n, (dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
|
||
dut.ram[32'h07B0 >> 2], dut.ram[32'h07AC >> 2], dut.ram[32'h08C0 >> 2]);
|
||
|
||
// 5) 通关后再点一下 = 返回背景(回到开始界面)
|
||
click_t;
|
||
#(3 * 1000000);
|
||
dump_fb("fb_14_back_to_start.txt");
|
||
|
||
$display(" [game] === 结束:state=%0d(1=WIN 2=LOSE)已揭开 %0d/%0d 旗=%0d 丢事件=%0d ===",
|
||
dut.ram[32'h07AC >> 2], open_n,
|
||
(dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
|
||
dut.ram[32'h07B0 >> 2], dut.ram[32'h08C0 >> 2]);
|
||
$finish;
|
||
end
|
||
|
||
endmodule
|