Files
risc-v-cpu/pipelined-cpu/srcs/tb/tb_saolei.v
T
Alpha 713ee99941 feat(saolei): 交互玩扫雷每一步自动出 PNG
- play_saolei.tcl: k 每执行一个按键动作就存 shots/NNN_<按键>.png,并覆盖
  shots/latest.png(在 VS Code 里开着 latest.png 即可每步自动刷新);新增 view 命令
- fb2png.py: 新增 --quiet(只写 PNG 不打印,供脚本批量截图)
- tb_saolei*.v: 仅格式调整(case 分支拆行),无语义变化
实测:k cuuuuc -> shots/001_c.png ... 006_c.png,每张 640x480 正常
2026-09-19 20:01:28 +08:00

456 lines
17 KiB
Verilog
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
`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