From 3f765ae4c527f18c95ebb2ca09636a5939e5aab3 Mon Sep 17 00:00:00 2001 From: Alex-create-beep <2965312221@qq.com> Date: Sat, 19 Sep 2026 19:26:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(saolei):=20=E4=BF=AE=E6=98=BE=E5=AD=98?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E8=AF=91=E7=A0=81=20+=20=E6=AE=B5=E9=97=B4?= =?UTF-8?q?=E7=A9=BA=E6=B4=9E=E5=A1=AB=20NOP;=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=97=A0=E6=98=BE=E7=A4=BA=E5=99=A8=E8=B0=83=E8=AF=95=E5=8F=B0?= =?UTF-8?q?(=E4=BB=BF=E7=9C=9F=E6=88=AA=E5=9B=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 热点直方图 --- pipelined-cpu/srcs/board/cpu21_board_top.v | 7 +- pipelined-cpu/srcs/board/cpu21_saolei_rom.vh | 288 +++++++++---------- pipelined-cpu/srcs/rtl/cpu21_mmio.v | 6 +- pipelined-cpu/srcs/tb/tb_saolei.v | 274 ++++++++++++++++++ saolei/tools/elf2hex.py | 15 + saolei/tools/fb2png.py | 157 ++++++++++ 6 files changed, 599 insertions(+), 148 deletions(-) create mode 100644 pipelined-cpu/srcs/tb/tb_saolei.v create mode 100644 saolei/tools/fb2png.py diff --git a/pipelined-cpu/srcs/board/cpu21_board_top.v b/pipelined-cpu/srcs/board/cpu21_board_top.v index 80848d5..a3c5ec0 100644 --- a/pipelined-cpu/srcs/board/cpu21_board_top.v +++ b/pipelined-cpu/srcs/board/cpu21_board_top.v @@ -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), diff --git a/pipelined-cpu/srcs/board/cpu21_saolei_rom.vh b/pipelined-cpu/srcs/board/cpu21_saolei_rom.vh index 3b2abf2..ca8b4fc 100644 --- a/pipelined-cpu/srcs/board/cpu21_saolei_rom.vh +++ b/pipelined-cpu/srcs/board/cpu21_saolei_rom.vh @@ -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; diff --git a/pipelined-cpu/srcs/rtl/cpu21_mmio.v b/pipelined-cpu/srcs/rtl/cpu21_mmio.v index 8dd9471..dbd5b80 100644 --- a/pipelined-cpu/srcs/rtl/cpu21_mmio.v +++ b/pipelined-cpu/srcs/rtl/cpu21_mmio.v @@ -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]; diff --git a/pipelined-cpu/srcs/tb/tb_saolei.v b/pipelined-cpu/srcs/tb/tb_saolei.v new file mode 100644 index 0000000..b33359e --- /dev/null +++ b/pipelined-cpu/srcs/tb/tb_saolei.v @@ -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 diff --git a/saolei/tools/elf2hex.py b/saolei/tools/elf2hex.py index 6ed5f98..d9da9d4 100644 --- a/saolei/tools/elf2hex.py +++ b/saolei/tools/elf2hex.py @@ -40,6 +40,21 @@ def main(): for i in range(0, len(data), 4): words.append(int.from_bytes(data[i:i + 4], "little")) + # 段间空洞填 NOP:objcopy 出来的 .bin 在 .text.isr1 / .text.isr2 / .text.isr3 + # 这些"绝对地址段"之间会留下 0x00000000,而本 CPU 把 0x00000000 译成自跳指令 + # (beq x0,x0,0 形式),PC 一旦落进去就永远出不来(仿真里已复现:PC 热点全在 + # 0x194-0x1FF / 0x218-0x2FF 这些空洞地址上)。这里把"最后一条非零字之前"的 + # 0 字统一换成 NOP,让 PC 即使误入也能顺势走过。 + last_nz = -1 + for i, w in enumerate(words): + if w != 0: + last_nz = i + filled = 0 + for i in range(last_nz): + if words[i] == 0: + words[i] = NOP + filled += 1 + if len(words) > words_total: print("错误:程序 %d 个字,超过 ROM 容量 %d 个字(改大 ROM_WORDS 或精简程序)" % (len(words), words_total)) diff --git a/saolei/tools/fb2png.py b/saolei/tools/fb2png.py new file mode 100644 index 0000000..c43f665 --- /dev/null +++ b/saolei/tools/fb2png.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""fb2png.py —— 把 tb_saolei 导出的显存 dump 还原成 PNG(等价于 VGA 截图)。 + +用法: + py saolei/tools/fb2png.py [输出.png] + +dump 格式(tb_saolei.v 里的 dump_fb 任务生成): + CURSOR + CTRL 0x # [0] 光标可见 [6:4] 背景色 + FB 1200 # 随后 1200 行十六进制字节,按格号 0..1199 + +渲染依据(与 RTL 严格对应,改 RTL 时这两个表要同步): + * 字库 cpu21_vga.v 的 font_row():8x8、bit7 是最左像素,放大 2 倍 = 16x16 一格 + * 调色板 cpu21_vga.v 的 pal():3 位 -> 4:4:4 RGB(0xA 分量 = 170) + * 光标 cpu21_vga.v 的 cursor_box:格子四边一圈(1 像素宽)显示浅灰 + * 画面 40x30 格 -> 640x480 + +另外还会在终端打印一份 ASCII 图,方便在没有图片查看器的地方快速看结果。 +""" + +import struct +import sys +import zlib + +COLS, ROWS = 40, 30 +CELL_PX = 16 # 8x8 字模放大 2 倍 + +# --- 字库(8 行/字,bit7 = 最左),抄自 cpu21_vga.v 的 font_row() --- +FONT = { + 0: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], # 空白 + 1: [0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00], # 1 + 2: [0x3C, 0x66, 0x06, 0x0C, 0x18, 0x30, 0x7E, 0x00], # 2 + 3: [0x3C, 0x66, 0x06, 0x1C, 0x06, 0x66, 0x3C, 0x00], # 3 + 4: [0x0C, 0x1C, 0x3C, 0x6C, 0x7E, 0x0C, 0x0C, 0x00], # 4 + 5: [0x7E, 0x60, 0x7C, 0x06, 0x06, 0x66, 0x3C, 0x00], # 5 + 6: [0x1C, 0x30, 0x60, 0x7C, 0x66, 0x66, 0x3C, 0x00], # 6 + 7: [0x7E, 0x66, 0x0C, 0x18, 0x18, 0x18, 0x18, 0x00], # 7 + 8: [0x3C, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x3C, 0x00], # 8 + 9: [0xFF, 0x81, 0xBD, 0xA5, 0xA5, 0xBD, 0x81, 0xFF], # 未揭开 + 10: [0x40, 0x70, 0x78, 0x70, 0x40, 0x40, 0x40, 0x00], # 旗 + 11: [0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C], # 雷 + 12: [0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81], # 爆炸 + 13: [0x3C, 0x66, 0x06, 0x0C, 0x18, 0x00, 0x18, 0x00], # 问号 + 14: [0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00], # 横线 + 15: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], # 实心 +} + +# --- 调色板(3 位 -> RGB),抄自 cpu21_vga.v 的 pal(),4 位分量按 *17 扩到 8 位 --- +PAL = [ + (0x0, 0x0, 0x0), # 0 黑 + (0x0, 0x0, 0xA), # 1 深蓝 + (0x0, 0xA, 0x0), # 2 绿 + (0x0, 0xA, 0xA), # 3 青 + (0xA, 0x0, 0x0), # 4 红 + (0xA, 0x0, 0xA), # 5 洋红 + (0xA, 0xA, 0x0), # 6 黄 + (0xA, 0xA, 0xA), # 7 浅灰 +] + +GLYPH_CH = {0: ' ', 9: '#', 10: 'P', 11: '*', 12: 'X', 13: '?', 14: '-', 15: '='} +COLOR_CH = {0: '.', 1: 'b', 2: 'g', 3: 'c', 4: 'r', 5: 'm', 6: 'y', 7: 'w'} + + +def write_png(path, width, height, rows): + """rows: 每行一个 bytes(长度 width*3),最小 PNG 编码。""" + raw = b"".join(b"\x00" + r for r in rows) + + def chunk(tag, data): + return (struct.pack(">I", len(data)) + tag + data + + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)) + + png = b"\x89PNG\r\n\x1a\n" + png += chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)) + png += chunk(b"IDAT", zlib.compress(raw, 9)) + png += chunk(b"IEND", b"") + with open(path, "wb") as f: + f.write(png) + + +def parse_dump(path): + cur = (None, None) + ctrl = 0 + fb = [] + with open(path, encoding="utf-8", errors="replace") as f: + for line in f: + line = line.strip() + if line.startswith("CURSOR"): + p = line.split() + cur = (int(p[1]), int(p[2])) + elif line.startswith("CTRL"): + ctrl = int(line.split()[1], 16) + elif line.startswith("FB"): + fb = [] + elif len(line) == 2 and fb is not None: + try: + fb.append(int(line, 16)) + except ValueError: + pass + if len(fb) != 1200: + raise SystemExit("显存字节数不对:%d(应为 1200)" % len(fb)) + return cur, ctrl, fb + + +def main(): + if len(sys.argv) < 2: + raise SystemExit(__doc__) + src = sys.argv[1] + dst = sys.argv[2] if len(sys.argv) > 2 else src.rsplit(".", 1)[0] + ".png" + cur, ctrl, fb = parse_dump(src) + + bg = (ctrl >> 4) & 0x7 + cursor_box = bool(ctrl & 1) and cur[0] is not None + print("光标 = (%s, %s) 光标可见 = %s 背景色 = %d" + % (cur[0], cur[1], bool(ctrl & 1), bg)) + + # ---- 渲染 ---- + rows = [] + for y in range(ROWS * CELL_PX): + row = bytearray() + cy, fy = y // CELL_PX, (y % CELL_PX) // 2 + for x in range(COLS * CELL_PX): + cx, fx = x // CELL_PX, (x % CELL_PX) // 2 + cell = fb[cy * COLS + cx] + g, c = cell & 0xF, (cell >> 4) & 0x7 + px_on = (FONT[g][fy] >> (7 - fx)) & 1 + col = c if px_on else bg + if cursor_box and (cx, cy) == cur: + # 格子四边一圈(1 像素)显示浅灰 + ox, oy = x % CELL_PX, y % CELL_PX + if ox == 0 or ox == CELL_PX - 1 or oy == 0 or oy == CELL_PX - 1: + col = 7 + r, gg, b = PAL[col] + row += bytes((r * 17, gg * 17, b * 17)) + rows.append(bytes(row)) + write_png(dst, COLS * CELL_PX, ROWS * CELL_PX, rows) + print("已生成 %s(%dx%d)" % (dst, COLS * CELL_PX, ROWS * CELL_PX)) + + # ---- 终端 ASCII 图 ---- + print("+" + "-" * COLS + "+") + for y in range(ROWS): + line = "" + for x in range(COLS): + cell = fb[y * COLS + x] + g, c = cell & 0xF, (cell >> 4) & 0x7 + if cursor_box and (x, y) == cur: + line += "[" + elif g in GLYPH_CH: + line += GLYPH_CH[g] + else: + line += chr(ord('0') + g) # 字形 1..8 就是数字 1..8 + line = line.replace("[", "[]" * 1) # 光标只提示左边界,避免打乱列数 + print("|" + (line + " " * COLS)[:COLS] + "|") + print("+" + "-" * COLS + "+") + print("图例: #=未揭开 P=旗 *=雷 X=爆炸 ?=问号 -=横线 ==实心 1..8=数字") + + +main()