`timescale 1ns / 1ps // ============================================================================= // tb_saolei_play —— 没有 VGA 显示器时"在命令行里玩扫雷"用的测试平台 // // 与 tb_saolei 的区别:没有写死的游戏脚本,改成受外部文件驱动: // // cmd.txt 外部(xsim 的 Tcl,见 pipelined-cpu/tools/play_saolei.tcl)往里写 // 按键字符;本 TB 每 100 us 仿真时间轮询一次,读到**一个**字符就执行 // 对应的按键脉冲,然后把文件清空。(多个动作用多次发送实现, // play_saolei.tcl 的 k 命令会自动把 "uuc" 拆成三次发。) // u/w 光标上移 d/s 光标下移 // l/a 光标左移 r 光标右移 // c 左键点击(BTNC) // f 插旗模式点击(先按 SW3 再点击,点上/取消旗帜) // p 只切换 SW3(插旗模式开关,不点击) // g 按板上的 go 键 // z 复位(拉低 rst_n 再放开,等于重新上电) // // now.txt 每 1 ms 仿真时间把当前显存 dump 一次(格式同 tb_saolei): // py saolei/tools/fb2png.py --term now.txt -> 终端里的彩色棋盘 // py saolei/tools/fb2png.py now.txt a.png -> 640x480 截图 // // 编译(两个 -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_play.v // xelab -d SAOLEI_ROM -top tb_saolei_play -snapshot saolei_play // xsim saolei_play -tclbatch tools/play_saolei.tcl // // 说明:和 tb_saolei 一样用 POINTER_SAMPLE_DIV=50 压缩按键去抖(上板仍是 50000)。 // ============================================================================= module tb_saolei_play; // ---- 板级顶层端口(全部由本 TB / cmd.txt 驱动,不用 add_force)---- 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; 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) ); // ---- 一个按键脉冲:按住 hold_us 再松开,然后留 200 us 让 CPU 消化事件 ---- task pulse; input integer which; input integer hold_us; begin case (which) 1: btnu = 1'b1; 2: btnd = 1'b1; 3: btnl = 1'b1; 4: btnr = 1'b1; default: go = 1'b1; endcase #(hold_us * 1000); case (which) 1: btnu = 1'b0; 2: btnd = 1'b0; 3: btnl = 1'b0; 4: btnr = 1'b0; default: go = 1'b0; endcase #(200 * 1000); end endtask // ---- 把 cmd.txt 里的一个字符变成按键动作 ---- // 注意:一次只取一个字符($fgetc),不用 $fgets/字符串, // 这样不会踩 "reg [8*N:1] 里第一个字符落在哪个字节" 的坑。 task do_key; input [7:0] ch; begin case (ch) 8'h75, 8'h77: pulse(1, 20); // u / w 上 8'h64, 8'h73: pulse(2, 20); // d / s 下 8'h6C, 8'h61: pulse(3, 20); // l / a 左 8'h72: pulse(4, 20); // r 右 8'h63: pulse(0, 30); // c 左键点击 8'h67: pulse(0, 30); // g go 键(同一次点击) 8'h66: begin // f 插旗模式点击 sw_flag = 1'b1; #(30 * 1000); pulse(0, 30); #(30 * 1000); sw_flag = 1'b0; #(30 * 1000); end 8'h70: begin // p 只切换插旗开关 sw_flag = ~sw_flag; #(50 * 1000); end 8'h7A: begin // z 复位 rst_n = 1'b0; #(2 * 1000); rst_n = 1'b1; #(500 * 1000); end default: ; // 其它字符忽略 endcase end endtask integer cfp; integer chc; // ---- 命令轮询:每 100 us 看一次 cmd.txt(只读第一个字符)---- initial begin #2000; rst_n = 1'b1; // 上电复位释放 forever begin #(100 * 1000); cfp = $fopen("cmd.txt", "r"); if (cfp != 0) begin chc = $fgetc(cfp); $fclose(cfp); cfp = $fopen("cmd.txt", "w"); // 处理掉就清空,避免重复执行 if (cfp != 0) $fclose(cfp); if ((chc > 0) && (chc != 8'h0A) && (chc != 8'h0D) && (chc != 8'h20)) do_key(chc[7:0]); end end end // ---- 画面 dump:每 1 ms 覆盖写一次 now.txt ---- integer df, w2, b2; initial begin forever begin #(1 * 1000 * 1000); df = $fopen("now.txt", "w"); if (df != 0) begin $fwrite(df, "TIME %0t\n", $time); $fwrite(df, "CURSOR %0d %0d\n", dut.u_vga.cursor_i[12:8], dut.u_vga.cursor_i[4:0]); $fwrite(df, "CTRL 0x%02X\n", dut.u_vga.ctrl_i[7:0]); $fwrite(df, "FB 1200\n"); for (w2 = 0; w2 < 300; w2 = w2 + 1) for (b2 = 0; b2 < 4; b2 = b2 + 1) $fwrite(df, "%02X\n", dut.fb[w2][b2*8 +: 8]); $fclose(df); end end end endmodule