Files
risc-v-cpu/pipelined-cpu/srcs/tb/tb_saolei.v
T
Alpha 3f765ae4c5 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 热点直方图
2026-09-19 19:26:26 +08:00

275 lines
10 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;
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