fix(saolei): 修显存窗口译码 + 段间空洞填 NOP; 新增无显示器调试台(仿真截图)

- cpu21_mmio.v: sel_fb 原写成 addr[12] && !addr[11](那是 0x1000-0x17FF),
  导致 0x2000+ 的显存读写全不命中, 而鼠标/显示寄存器写反而落进显存。
  改为 bit13=1 && bit12=0 && bit11=0 后扫雷画面才正常:
  修复前仿真 fb_we 只有 6 次写(画面全空), 修复后开始界面 1246 次、进关卡后 2775 次
- cpu21_board_top.v: 新增 POINTER_SAMPLE_DIV 参数(默认仍为上板的 50000),
  仿真传 50 即可让按键在几十微秒内被识别(defparam 对 generate 块内实例无效)
- saolei/tools/elf2hex.py: .text.isr1/2/3 之间的空洞(0 字)填 NOP —— objcopy 留下的
  0x00000000 在本 CPU 上等同自跳指令, PC 掉进去就出不来
- saolei/tools/fb2png.py + srcs/tb/tb_saolei.v: 无 VGA 显示器的调试台 —— 跑板级顶层、
  按键当鼠标、把 40x30 显存 dump 成文本, 再按 cpu21_vga.v 的字库/调色板还原 PNG,
  并附 ASCII 图 + RAM 变量视图 + PC 热点直方图
This commit is contained in:
2026-09-19 19:26:26 +08:00
parent 4ef3960553
commit 3f765ae4c5
6 changed files with 599 additions and 148 deletions
+5 -2
View File
@@ -53,7 +53,10 @@ module cpu21_board_top #(
// 所以不会掩盖时序违例(约束偏严)。
parameter integer CLK_DIV_INTRUPT = 32,
// 鼠标事件源:0 = 板上 5 向按键(免外设,默认);1 = PS/2 鼠标(Pmod JA)
parameter integer USE_PS2_MOUSE = 0
parameter integer USE_PS2_MOUSE = 0,
// 5 向按键去抖的采样周期(CPU 周期数):上板默认 50000(1 ms @50 MHz)。
// 仿真里可以传一个小值,让"按一下"在几十微秒内就被识别。
parameter integer POINTER_SAMPLE_DIV = 50000
) (
input wire clk100, // E3
input wire rst_n, // C12:CPU_RESETN,低有效
@@ -451,7 +454,7 @@ module cpu21_board_top #(
end
else begin : g_btn
cpu21_pointer #(
.SAMPLE_DIV(50000)
.SAMPLE_DIV(POINTER_SAMPLE_DIV)
) u_ptr (
.clk (clk_cpu),
.reset (reset),
+144 -144
View File
@@ -5,65 +5,65 @@
rom[2] = 32'h000012B7;
rom[3] = 32'hE3028293;
rom[4] = 32'h00028067;
rom[5] = 32'h00000000;
rom[6] = 32'h00000000;
rom[7] = 32'h00000000;
rom[8] = 32'h00000000;
rom[9] = 32'h00000000;
rom[10] = 32'h00000000;
rom[11] = 32'h00000000;
rom[12] = 32'h00000000;
rom[13] = 32'h00000000;
rom[14] = 32'h00000000;
rom[15] = 32'h00000000;
rom[16] = 32'h00000000;
rom[17] = 32'h00000000;
rom[18] = 32'h00000000;
rom[19] = 32'h00000000;
rom[20] = 32'h00000000;
rom[21] = 32'h00000000;
rom[22] = 32'h00000000;
rom[23] = 32'h00000000;
rom[24] = 32'h00000000;
rom[25] = 32'h00000000;
rom[26] = 32'h00000000;
rom[27] = 32'h00000000;
rom[28] = 32'h00000000;
rom[29] = 32'h00000000;
rom[30] = 32'h00000000;
rom[31] = 32'h00000000;
rom[32] = 32'h00000000;
rom[33] = 32'h00000000;
rom[34] = 32'h00000000;
rom[35] = 32'h00000000;
rom[36] = 32'h00000000;
rom[37] = 32'h00000000;
rom[38] = 32'h00000000;
rom[39] = 32'h00000000;
rom[40] = 32'h00000000;
rom[41] = 32'h00000000;
rom[42] = 32'h00000000;
rom[43] = 32'h00000000;
rom[44] = 32'h00000000;
rom[45] = 32'h00000000;
rom[46] = 32'h00000000;
rom[47] = 32'h00000000;
rom[48] = 32'h00000000;
rom[49] = 32'h00000000;
rom[50] = 32'h00000000;
rom[51] = 32'h00000000;
rom[52] = 32'h00000000;
rom[53] = 32'h00000000;
rom[54] = 32'h00000000;
rom[55] = 32'h00000000;
rom[56] = 32'h00000000;
rom[57] = 32'h00000000;
rom[58] = 32'h00000000;
rom[59] = 32'h00000000;
rom[60] = 32'h00000000;
rom[61] = 32'h00000000;
rom[62] = 32'h00000000;
rom[63] = 32'h00000000;
rom[5] = 32'h00000013;
rom[6] = 32'h00000013;
rom[7] = 32'h00000013;
rom[8] = 32'h00000013;
rom[9] = 32'h00000013;
rom[10] = 32'h00000013;
rom[11] = 32'h00000013;
rom[12] = 32'h00000013;
rom[13] = 32'h00000013;
rom[14] = 32'h00000013;
rom[15] = 32'h00000013;
rom[16] = 32'h00000013;
rom[17] = 32'h00000013;
rom[18] = 32'h00000013;
rom[19] = 32'h00000013;
rom[20] = 32'h00000013;
rom[21] = 32'h00000013;
rom[22] = 32'h00000013;
rom[23] = 32'h00000013;
rom[24] = 32'h00000013;
rom[25] = 32'h00000013;
rom[26] = 32'h00000013;
rom[27] = 32'h00000013;
rom[28] = 32'h00000013;
rom[29] = 32'h00000013;
rom[30] = 32'h00000013;
rom[31] = 32'h00000013;
rom[32] = 32'h00000013;
rom[33] = 32'h00000013;
rom[34] = 32'h00000013;
rom[35] = 32'h00000013;
rom[36] = 32'h00000013;
rom[37] = 32'h00000013;
rom[38] = 32'h00000013;
rom[39] = 32'h00000013;
rom[40] = 32'h00000013;
rom[41] = 32'h00000013;
rom[42] = 32'h00000013;
rom[43] = 32'h00000013;
rom[44] = 32'h00000013;
rom[45] = 32'h00000013;
rom[46] = 32'h00000013;
rom[47] = 32'h00000013;
rom[48] = 32'h00000013;
rom[49] = 32'h00000013;
rom[50] = 32'h00000013;
rom[51] = 32'h00000013;
rom[52] = 32'h00000013;
rom[53] = 32'h00000013;
rom[54] = 32'h00000013;
rom[55] = 32'h00000013;
rom[56] = 32'h00000013;
rom[57] = 32'h00000013;
rom[58] = 32'h00000013;
rom[59] = 32'h00000013;
rom[60] = 32'h00000013;
rom[61] = 32'h00000013;
rom[62] = 32'h00000013;
rom[63] = 32'h00000013;
rom[64] = 32'hFC010113;
rom[65] = 32'h00112023;
rom[66] = 32'h00512223;
@@ -101,97 +101,97 @@
rom[98] = 32'h00012083;
rom[99] = 32'h04010113;
rom[100] = 32'h00200073;
rom[101] = 32'h00000000;
rom[102] = 32'h00000000;
rom[103] = 32'h00000000;
rom[104] = 32'h00000000;
rom[105] = 32'h00000000;
rom[106] = 32'h00000000;
rom[107] = 32'h00000000;
rom[108] = 32'h00000000;
rom[109] = 32'h00000000;
rom[110] = 32'h00000000;
rom[111] = 32'h00000000;
rom[112] = 32'h00000000;
rom[113] = 32'h00000000;
rom[114] = 32'h00000000;
rom[115] = 32'h00000000;
rom[116] = 32'h00000000;
rom[117] = 32'h00000000;
rom[118] = 32'h00000000;
rom[119] = 32'h00000000;
rom[120] = 32'h00000000;
rom[121] = 32'h00000000;
rom[122] = 32'h00000000;
rom[123] = 32'h00000000;
rom[124] = 32'h00000000;
rom[125] = 32'h00000000;
rom[126] = 32'h00000000;
rom[127] = 32'h00000000;
rom[101] = 32'h00000013;
rom[102] = 32'h00000013;
rom[103] = 32'h00000013;
rom[104] = 32'h00000013;
rom[105] = 32'h00000013;
rom[106] = 32'h00000013;
rom[107] = 32'h00000013;
rom[108] = 32'h00000013;
rom[109] = 32'h00000013;
rom[110] = 32'h00000013;
rom[111] = 32'h00000013;
rom[112] = 32'h00000013;
rom[113] = 32'h00000013;
rom[114] = 32'h00000013;
rom[115] = 32'h00000013;
rom[116] = 32'h00000013;
rom[117] = 32'h00000013;
rom[118] = 32'h00000013;
rom[119] = 32'h00000013;
rom[120] = 32'h00000013;
rom[121] = 32'h00000013;
rom[122] = 32'h00000013;
rom[123] = 32'h00000013;
rom[124] = 32'h00000013;
rom[125] = 32'h00000013;
rom[126] = 32'h00000013;
rom[127] = 32'h00000013;
rom[128] = 32'hFF810113;
rom[129] = 32'h00112023;
rom[130] = 32'h4A1000EF;
rom[131] = 32'h00012083;
rom[132] = 32'h00810113;
rom[133] = 32'h00200073;
rom[134] = 32'h00000000;
rom[135] = 32'h00000000;
rom[136] = 32'h00000000;
rom[137] = 32'h00000000;
rom[138] = 32'h00000000;
rom[139] = 32'h00000000;
rom[140] = 32'h00000000;
rom[141] = 32'h00000000;
rom[142] = 32'h00000000;
rom[143] = 32'h00000000;
rom[144] = 32'h00000000;
rom[145] = 32'h00000000;
rom[146] = 32'h00000000;
rom[147] = 32'h00000000;
rom[148] = 32'h00000000;
rom[149] = 32'h00000000;
rom[150] = 32'h00000000;
rom[151] = 32'h00000000;
rom[152] = 32'h00000000;
rom[153] = 32'h00000000;
rom[154] = 32'h00000000;
rom[155] = 32'h00000000;
rom[156] = 32'h00000000;
rom[157] = 32'h00000000;
rom[158] = 32'h00000000;
rom[159] = 32'h00000000;
rom[160] = 32'h00000000;
rom[161] = 32'h00000000;
rom[162] = 32'h00000000;
rom[163] = 32'h00000000;
rom[164] = 32'h00000000;
rom[165] = 32'h00000000;
rom[166] = 32'h00000000;
rom[167] = 32'h00000000;
rom[168] = 32'h00000000;
rom[169] = 32'h00000000;
rom[170] = 32'h00000000;
rom[171] = 32'h00000000;
rom[172] = 32'h00000000;
rom[173] = 32'h00000000;
rom[174] = 32'h00000000;
rom[175] = 32'h00000000;
rom[176] = 32'h00000000;
rom[177] = 32'h00000000;
rom[178] = 32'h00000000;
rom[179] = 32'h00000000;
rom[180] = 32'h00000000;
rom[181] = 32'h00000000;
rom[182] = 32'h00000000;
rom[183] = 32'h00000000;
rom[184] = 32'h00000000;
rom[185] = 32'h00000000;
rom[186] = 32'h00000000;
rom[187] = 32'h00000000;
rom[188] = 32'h00000000;
rom[189] = 32'h00000000;
rom[190] = 32'h00000000;
rom[191] = 32'h00000000;
rom[134] = 32'h00000013;
rom[135] = 32'h00000013;
rom[136] = 32'h00000013;
rom[137] = 32'h00000013;
rom[138] = 32'h00000013;
rom[139] = 32'h00000013;
rom[140] = 32'h00000013;
rom[141] = 32'h00000013;
rom[142] = 32'h00000013;
rom[143] = 32'h00000013;
rom[144] = 32'h00000013;
rom[145] = 32'h00000013;
rom[146] = 32'h00000013;
rom[147] = 32'h00000013;
rom[148] = 32'h00000013;
rom[149] = 32'h00000013;
rom[150] = 32'h00000013;
rom[151] = 32'h00000013;
rom[152] = 32'h00000013;
rom[153] = 32'h00000013;
rom[154] = 32'h00000013;
rom[155] = 32'h00000013;
rom[156] = 32'h00000013;
rom[157] = 32'h00000013;
rom[158] = 32'h00000013;
rom[159] = 32'h00000013;
rom[160] = 32'h00000013;
rom[161] = 32'h00000013;
rom[162] = 32'h00000013;
rom[163] = 32'h00000013;
rom[164] = 32'h00000013;
rom[165] = 32'h00000013;
rom[166] = 32'h00000013;
rom[167] = 32'h00000013;
rom[168] = 32'h00000013;
rom[169] = 32'h00000013;
rom[170] = 32'h00000013;
rom[171] = 32'h00000013;
rom[172] = 32'h00000013;
rom[173] = 32'h00000013;
rom[174] = 32'h00000013;
rom[175] = 32'h00000013;
rom[176] = 32'h00000013;
rom[177] = 32'h00000013;
rom[178] = 32'h00000013;
rom[179] = 32'h00000013;
rom[180] = 32'h00000013;
rom[181] = 32'h00000013;
rom[182] = 32'h00000013;
rom[183] = 32'h00000013;
rom[184] = 32'h00000013;
rom[185] = 32'h00000013;
rom[186] = 32'h00000013;
rom[187] = 32'h00000013;
rom[188] = 32'h00000013;
rom[189] = 32'h00000013;
rom[190] = 32'h00000013;
rom[191] = 32'h00000013;
rom[192] = 32'h00200073;
rom[193] = 32'h00000793;
rom[194] = 32'h59C00713;
+4 -2
View File
@@ -96,8 +96,10 @@ module cpu21_mmio #(
wire sel_seg = (addr_i[31:2] == SEG_ADDR[31:2]);
wire sel_mouse = (addr_i[31:8] == MOUSE_BASE[31:8]) && (addr_i[7:6] == 2'b00);
wire sel_disp = (addr_i[31:8] == DISP_BASE[31:8]) && (addr_i[7:6] == 2'b01);
// 显存窗口 0x2000 - 0x27FF(addr[12]=1 且 addr[11]=0)
wire sel_fb = (addr_i[31:13] == 19'd0) && addr_i[12] && !addr_i[11];
// 显存窗口 0x2000 - 0x27FF:bit13=1、bit12=0、bit11=0,高位全 0
// (原来写成 addr[12] && !addr[11],要求 bit12=1 —— 那是 0x1000-0x17FF,
// 于是 0x2000+ 的读写全都不命中、而鼠标/显示寄存器写反而落进了显存)
wire sel_fb = (addr_i[31:14] == 18'd0) && addr_i[13] && !addr_i[12] && !addr_i[11];
wire [3:0] midx = addr_i[5:2];
wire [3:0] didx = addr_i[5:2];
+274
View File
@@ -0,0 +1,274 @@
`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;
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;
// ---- 探针:PC 什么时候"跳进" ISR 段之间的空洞(0x194-0x1FF / 0x218-0x2FF)----
// 这些空洞在 ROM 里是 0x00000000,在本 CPU 上会被当成自跳指令,PC 掉进去出不来。
reg [31:0] pc_prev;
initial pc_prev = 32'hFFFF_FFFF;
always @(posedge dut.clk_cpu) begin
pc_prev <= dut.u_cpu.pc_q;
if (dut.u_cpu.pc_q != (pc_prev + 32'd4) &&
((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
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
// ---------------------------------------------------------------- 主流程
initial begin
$display("[tb_saolei] === start (SAOLEI_ROM) ===");
#2000;
rst_n = 1'b1;
// 1) 背景开始界面:光标初始就停在 START 按钮上(行 20,列 17..22)
#(2 * 1000000);
dump_fb("fb_00_start.txt");
// 2) 验证移动的方向与步长:下 2 格、右 2 格,再原路回位
repeat (2) press(2, 200);
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);
dump_fb("fb_03_level.txt");
// 4) 关卡内:往下 2 格、往右 2 格,再点一下揭开
repeat (2) press(2, 200);
repeat (2) press(4, 200);
dump_fb("fb_04_cursor.txt");
click;
#(2 * 1000000);
dump_fb("fb_05_firstclick.txt");
// 5) 插旗(SW3 = 插旗模式 + 左键)
sw_flag = 1'b1;
#(100 * 1000);
click;
#(2 * 1000000);
sw_flag = 1'b0;
dump_fb("fb_06_flag.txt");
// 6) 往右一格再揭开
press(4, 200);
click;
#(2 * 1000000);
dump_fb("fb_07_click2.txt");
$display("[tb_saolei] === done, LED=%04h led_we=%b ===", led, led_we);
$finish;
end
endmodule