简介:这是一份面向FPGA学习者和电子设计爱好者的俄罗斯方块游戏完整工程合集,包含Verilog与VHDL两种硬件描述语言版本,并分别基于Vivado与ISE两种开发环境构建,适合用于数字逻辑课程设计、FPGA入门实践或毕业设计参考。资源共405个文件,压缩包18.15MB,涵盖.v/.vhd源码、.xpr/.xise工程文件、.ucf/.xdc约束文件、.bit/.sof配置文件以及仿真波形、综合报告、脚本等,能够覆盖从代码编写到下载验证的主要流程。已有3077人学习,是验证硬件描述语言和FPGA开发流程的典型项目。通过对比不同语言和工具链下的实现,可以学习状态机设计、VGA显示控制、键盘输入处理、碰撞检测等游戏逻辑,同时体会Vivado与ISE在工程管理、综合实现和资源利用上的差异。对于想通过实际项目理解数字系统设计、并行硬件加速和接口时序控制的读者,这份资源能提供从仿真到上板验证的完整参考。
1. 一个标题里塞进五套工程:FPGA 俄罗斯方块最该读的不是代码,是版本
如果你照着网上流传的单个 FPGA 俄罗斯方块工程去练手,大概率会走到同一个死胡同:仿真里转得挺欢,一下板画面斜着滚、按键失灵、方块卡在墙里,最后只能怀疑人生。标题里这五套工程最值钱的地方,不是它把俄罗斯方块做出来了,而是把“纯逻辑核 → VGA 显示 → 完整可玩 → VHDL 移植 → 老工具链移植”这条路完整铺开了。Verilog 和 VHDL 各有一版,Vivado 和 ISE 也都能对上号,适合正在选语言、选开发板、或者被工具链迁移折腾的人。我建议你把版本当路线图读,别一上来就挑一套最复杂的开跑。
2. FPGA 俄罗斯方块的设计骨架:为什么游戏核和显示核必须分开写
先搞清楚一件事:俄罗斯方块在 FPGA 里不是“画出来的”,是“算出来的”。屏幕上每一个格子,对应的是内存里的一位;方块的移动、旋转、消行,都是对这片内存做读写。如果你把显示逻辑和游戏逻辑揉在同一个 always 块里,后面加一个计分功能都可能让你改得头皮发麻。
无论哪一个版本,工程基本都长这样:游戏核负责维护 10x20 的场矩阵,输入核处理按键和消抖,显示核按 VGA 时序把场矩阵扫到屏幕上。这个边界画对了,后面的版本切换、语言移植、工具链迁移,痛点才会少一大半。
2.1 方块不是图形是数据:Tetromino 与 10x20 场矩阵怎么表示
场矩阵最常用的表示是每行一个寄存器位宽为 10,总共 20 行。field[0]是最底行,field[19]是最顶行,某一位为 1 表示该格已被占用。这样写的好处是消行判断特别直接:field[gy] == 10'h3FF就说明这一行满了。
方块本身不要存成大的二维矩阵,更省资源的做法是只存四个格子的相对坐标。每个方块本质是 4 个点,旋转就是把这 4 个点绕着 4x4 网格的中心转 90 度。比如一个方块横放时四个格子的相对坐标是(0,0)(1,0)(2,0)(3,0),顺时针旋转后就变成(0,0)(0,1)(0,2)(0,3)。这个思路在 Verilog 和 VHDL 里都能直接用坐标数组表示,省掉一大片二维数组索引的烦恼。
实际工程里一般用一个寄存器数组存当前活动块的shape_x[0:3]和shape_y[0:3],再用一个编号去查表初始化。查表用 case 写,可综合,比在 initial 里赋值靠谱得多。很多初学者栽在形状数据上,就是因为把方块定义写进了 initial,仿真能过、综合出一堆冗余逻辑。
2.2 碰撞、旋转与消行:三段最该逐行读的核心代码
碰撞检测是这个项目里最值得反复读的二十行。它做的事很简单:把活动块的四个格子的绝对坐标算出来,任何一个越界或者落到已占用格子上,就算碰撞。
// 碰撞检测:逐格遍历活动块的四个组成格子 integer i, gx, gy; reg collide; always @(*) begin collide = 1'b0; for (i = 0; i < 4; i = i + 1) begin gx = cur_x + shape_x[i]; // 绝对列坐标 gy = cur_y + shape_y[i]; // 绝对行坐标 if (gx < 0 || gx > 9 || gy > 19) begin collide = 1'b1; // 碰左右墙或碰底 end else if (field[gy][gx]) begin collide = 1'b1; // 碰已有方块 end end end这里cur_x和cur_y是活动块左上参考点在场地里的坐标,配合四个相对坐标使用。之所以用gy > 19而不是gy >= 20,是为了把一整段判断写在一个 if 里,减少综合后的 LUT 级联。collide是纯组合信号,必须在同一个时钟周期内被下落的判定逻辑采样,不能打一拍再用,否则急停的时候会多走一格。
再看旋转。用坐标表做旋转,代码只有两行,但方向最容易错:
// 顺时针旋转 90 度:新 x = 3 - 旧 y,新 y = 旧 x next_x[i] = 3 - shape_y[i]; next_y[i] = shape_x[i];注意旋转之后不能直接替换当前形状,要先做一次碰撞试探:用next_x/next_y配合当前cur_x/cur_y去跑一遍碰撞检测,如果碰撞,再尝试把cur_x左移或右移一到两格,继续试探。这样做是为了让贴墙的方块也能旋转,而不是旋转失败就静默丢弃。五个版本里旋转逻辑基本都遵循这个流程,差别只在试探范围和碰撞检测的写法。
消行逻辑是另一个容易翻车的地方。最直接的理解是“从底向上扫,满一行就把上面所有行往下搬一行”。但真实工程里会有一个细节:连续消多行时,同一拍内只能搬一次,否则会出现整列错位。
// 简化示意:满行检测 + 整行下移,真实工程需要按消行数做移位 for (r = 19; r >= 1; r = r - 1) begin if (field[r] == 10'h3FF) begin for (c = 0; c < 10; c = c + 1) field[r][c] <= field[r-1][c]; end end这段代码只适合理解思路,不适合直接抄进状态机。真正稳的写法是先在一个组合块里把满行数量算出来,再用移位寄存器或者循环逐行搬移,搬移方向必须自顶向下,否则连续消行时会吞掉未满的行。五个版本里,V3 之后的版本都是按这个方式处理的,V1 和 V2 只处理单行消行。
2.3 五版共用模块图:游戏核、显示核、输入核的接口约定
| 模块 | 主要输入 | 主要输出 | 职责 |
|---|---|---|---|
| game_core | fall_tick、key_left/right/down/rotate | field[19:0]、score、level | 维护场矩阵与活动块状态 |
| input_debounce | clk、btn_in | btn_stable、btn_press | 同步、消抖、边沿检测 |
| vga_timing | clk_pix、field | hsync、vsync、rgb | 时序产生 + 格子绘制 |
| score_display | score、clk | seg_data / vga_overlay | 分数与等级显示 |
这个接口划分是五个版本的公共骨架。你拿任何一个版本打开,顶层大体都是这四个模块再加一个例化层。区分版本的不是接口,而是内部实现粒度:有的把 game_core 写成了一个大状态机,有的拆成了 control 和 collide 两个子模块,有的把 VGA 绘制逻辑写进了纯组合逻辑。
阅读顺序上,我建议你从 game_core 入手,其次是 input_debounce,最后才是 vga_timing。因为 VGA 部分只要时序对了,就是搬运场矩阵,难点只在格子坐标换算,而游戏核才是整个工程的魂。
3. 五个版本逐个抠细节:Verilog 与 VHDL 各自的门道
标题里五套工程不是同一代码的五个拷贝,而是按开发阶段递进的。我的建议是先在工程里建一个版本对照文档,记下每个版本改了哪些模块,不然你会在三个相似文件之间反复跳转,最后连自己在看哪个版本都忘了。
3.1 V1 纯逻辑验证核:先不碰 VGA,把规则跑通再说
V1 是最小可运行版本,典型做法是去掉 VGA,只保留 game_core 和几个 LED 输出。按键接板上按钮,方块是否落地、是否消行,通过 LED 亮灭或者数码管观察。这个版本看着简陋,但它有一个杀手级优势:仿真速度极快,适合验证旋转、碰撞、消行这些核心规则。
// V1 顶层:不带 VGA,用 LED 指示底部三行的占用情况 module tetris_v1( input wire clk_50m, input wire btn_rotate, output wire [9:0] led_field ); // 内部例化 game_core,把场矩阵最低一行拉出来看 assign led_field = field[0]; endmodule这个版本里最需要注意的是:没有显示约束,你只能靠仿真波形验证逻辑。写一个简单的 testbench,往 game_core 里灌固定的按键序列,然后查 field 矩阵在固定周期的值。如果 V1 阶段的碰撞检测能过仿真,后面所有版本的核心逻辑都不会有大问题。
3.2 V2 带 VGA 的最小可玩版:先从 640x480 硬时序扣起
V2 加上 VGA,画面开始能看了,但大部分人的第一个坑也在这:屏幕斜着滚、画面撕裂、上下跳动。不是游戏逻辑错,而是 VGA 时序参数不对。640x480@60Hz 的标准参数是死的,像素时钟用 25MHz 或者 25.175MHz,一行 800 个像素时钟,其中有效数据 640 个,一帧 525 行,其中有效显示 480 行。
// VGA 行时序:h_cnt 从 0 数到 799 localparam H_ACTIVE = 640; localparam H_FRONT = 16; localparam H_SYNC = 96; localparam H_BACK = 48; always @(posedge clk_pix) begin if (h_cnt == H_ACTIVE + H_FRONT + H_SYNC + H_BACK - 1) h_cnt <= 0; else h_cnt <= h_cnt + 1; end assign hsync = (h_cnt >= H_ACTIVE + H_FRONT) && (h_cnt < H_ACTIVE + H_FRONT + H_SYNC);clk_pix是像素时钟,不要把 50MHz 系统时钟直接拿去驱动 VGA。常见的做法是给每个时钟周期打一个使能,或者用一个 PLL 生成 25MHz。如果直接用 50MHz,可以把 h_cnt 的计数范围翻倍、hsync 的位置也翻倍,效果等价,但代码可读性会差很多。V2 版本里把绘制区域和游戏的格子坐标做一次换算,比如每个格子用 20x20 像素,游戏区左上角偏到屏幕中央偏左的位置,这个偏移量写在常量里,后续版本都复用同一种换算方式。
3.3 V3 完整 Verilog 版:消抖、7-bag 随机、计分与等级
V3 是可以拿出去展示的完整版。这一版补上了三样东西:按键消抖与边沿检测、7-bag 随机出块、计分与等级加速。其中下落速度不再是固定拍数,而是根据等级查表得到一个分频阈值。
// 下落时钟分频:等级越高,阈值越小,下落越快 localparam [19:0] SPEED_TABLE [0:9] = '{ 20'd1_000_000, // level 0 20'd800_000, // level 1 20'd650_000, // ... 递减 }; always @(posedge clk) begin if (tick_cnt >= SPEED_TABLE[level]) begin tick_cnt <= 0; fall_tick <= 1'b1; end else begin tick_cnt <= tick_cnt + 1; fall_tick <= 1'b0; end end这里有个小细节:fall_tick不能只拉高一个周期就完事,要确保它被 game_core 在下一个时钟沿采样到。更稳的做法是把下落控制和 game_core 的状态机联动,fall_tick作为状态跳转条件之一,而不是一个独立的脉冲信号。很多改版工程在这里翻车,表现为方块“瞬移”两格,那就是 tick 信号没同步,被两个模块各采样了一次。
随机出块方面,V3 用了 7-bag 系统:把 7 种方块编号放到一个队列里,每次从中抽一个,抽完重新洗牌。这样保证七个方块内不会出现连续三个同种方块,游戏公平性比纯 LFSR 随机好得多。LFSR 仍然用作洗牌时的随机源,值得注意的是 LFSR 的初值不能全零,否则整个序列锁死,这就是随机模块里最经典的“玄学”问题。
3.4 V4 VHDL 移植版:当索引下标和类型系统站到你对面
V4 是把 Verilog 版翻译成 VHDL 的对照工程,也是五个版本里最容易暴露“语言思维差异”的一版。VHDL 没有 Verilog 那种自由的reg [3:0] shape [0:3]写法,必须先把类型定义清楚。
type shape_t is array (0 to 3) of std_logic_vector(3 downto 0); constant SHAPE_I_H : shape_t := ( "0000", "1111", "0000", "0000" );这还只是静态数据。真正写旋转逻辑时,VHDL 的 function 需要一个返回数组类型的函数,不能像 Verilog 那样直接在 always 里用临时变量赋值。V4 版本的旋转函数是这么组织的:
function rotate_cw(s : shape_t) return shape_t is variable r : shape_t; begin for i in 0 to 3 loop for j in 0 to 3 loop r(i, j) := s(3 - j, i); end loop; end loop; return r; end function;注意 VHDL 的二维数组下标顺序是(row, col),而 Verilog 是[row][col],翻译的时候最容易在这里踩坑,差一个下标就是你按了旋转键,方块直接变成一团噪声。V4 的碰撞检测和消行逻辑,建议对照 V3 逐模块翻译,不要自己重新发挥。因为你一旦“优化”了逻辑,就会引入 Verilog 里没有、VHDL 里更隐蔽的进程敏感列表问题。
3.5 V5 ISE 老工程兼容版:给还在用 Spartan-6 的人留的路
V5 面向 ISE 环境,通常还是老器件。这一版最大的价值在于:Vivado 的工程结构、约束格式和 IP 生成方式是全新的,如果你拿 V3 的源文件直接拖进 ISE,会有大量语法和 IP 兼容问题,而 V5 提供了一个能直接落地的起点。
ISE 工程里没有什么.xpr,打开工程靠.xise文件;MIG 和 PLL 这类 IP 也是用 CoreGen 生成的.xco,和 Vivado 的 IP 目录完全不是一个格式。V5 版本的做法很朴素:不依赖任何需要重新生成的 IP,像素时钟直接用板上 50MHz 做分频使能,存储用分布式 RAM 或 BRAM 的简单推断,而不是例化专用 IP。这让工程在 ISE 14.7 里能直接综合。
另外一个 ISE 特有的问题是 XST 综合器对 Verilog 语法的兼容度比 Vivado 旧一些,比如+:索引、多维数组作为端口这类写法在 XST 里表现不稳定。V5 里把这些地方都改成了最保守的写法,如果你打算从 V4 或 V3 移植到老工程,保守写法是必须过的一关。
4. 从 ISE 迁到 Vivado:约束、IP 与综合选项的七处改动
“我在 ISE 里能综合,怎么到 Vivado 全变红了?”这是老工程迁移最常见的一句抱怨。Vivado 和 ISE 虽然都是同一家的工具,但工程体系几乎是两代人。五套工程里 V5 是用 ISE 组织的,其余用 Vivado,对比着看正好把迁移过程反推出来。
4.1 工程文件与目录结构:两代工具的存档习惯完全不同
ISE 工程里最核心的是.xise工程文件,源文件列表、综合选项、约束文件挂载全在里面。Vivado 用.xpr,其实是一个 Tcl 脚本和一组元数据文件的集合,后边还跟着.srcs、.runs、.gen这些目录。
迁移时不要试图“转换工程文件”,没有这个选项。正确的步骤是:新建一个 Vivado 工程,把 Verilog/VHDL 源文件通过 Add Sources 加进去,然后重新添加约束,重新生成 IP。如果你直接把.ise文件拖进 Vivado,它会直接拒绝打开。
# 在 Vivado Tcl 控制台里建工程并加入源文件 create_project tetris_vivado ./tetris_vivado -part xc7a35tcsg324-1 add_files -norecurse ./src/tetris_top.v add_files -norecurse ./src/game_core.v add_files -norecurse ./src/vga_timing.v update_compile_order -fileset sources_1这段命令里-part必须换成你自己的器件型号,x开头的器件是 7 系列,V5 的老器件不适用。update_compile_order会让 Vivado 自动处理文件依赖顺序,这一步不能省,否则综合时它会按字母序编译,而 Verilog 对模块定义顺序并不敏感,VHDL 则会直接报 entity 找不到。
4.2 约束文件从 UCF 到 XDC:写法几乎推倒重来
ISE 时代约束文件叫 UCF,Vivado 用 XDC。语法差异比我预想的还要大,管脚约束、时钟约束全部换了写法。
UCF 的管脚写法是:
NET "clk_50m" LOC = "T8" | IOSTANDARD = LVCMOS33; NET "btn_left" LOC = "R7" | IOSTANDARD = LVCMOS33;同样的约束在 XDC 里长这样:
set_property PACKAGE_PIN T8 [get_ports clk_50m] set_property IOSTANDARD LVCMOS33 [get_ports clk_50m] set_property PACKAGE_PIN R7 [get_ports btn_left] set_property IOSTANDARD LVCMOS33 [get_ports btn_left]UCF 的一个NET语句把管脚位置和电平标准放在一起,XDC 用两条set_property分开设置。这看起来只是语法差异,但实际迁移时真正的坑在端口名:ISE 里内部信号可以直接约束,Vivado 的get_ports只能获取顶层端口,如果你原来约束的是某个内部网络,迁到 Vivado 后需要先改成端口名,或者用get_nets。
时钟约束也要重写。UCF 里的周期约束通常写作:
NET "clk_50m" TNM_NET = clk_50m; TIMESPEC TS_clk_50m = PERIOD clk_50m 20 ns;XDC 是create_clock:
create_clock -period 20.000 -name clk_50m [get_ports clk_50m]注意-name后面的名字只是时钟名,不是端口名,可以自己起。迁移后如果看到关于clk的时序异常,先查是不是 create_clock 没写对,或者时钟名字引用不统一。
4.3 Clocking Wizard、Block RAM 与综合选项:IP 是重灾区
Vivado 和 ISE 的 IP 体系完全不相容。ISE 里用 CoreGen 生成的.xco文件,Vivado 不认,必须在 Vivado 的 IP Catalog 里重新生成。如果是 PLL 或 MMCM,重新生成后要注意两个信号:locked 引脚和 clk_out 的相位关系。不同工具生成的 MMCM 输出时序可能差几十皮秒,对于 25MHz 像素时钟来说不影响 VGA,但如果时钟还驱动了游戏逻辑,最好把 locked 作为复位条件的一部分,确保时钟稳定后再跑逻辑。
BRAM 也一样。ISE 的 Block Memory Generator 生成的工程里带一大堆辅助文件,Vivado 里只需要一个 XCI 文件,其余全部自动生成。我一般建议俄罗斯方块这种小工程直接不要用 IP,用手写寄存器数组让工具自动推断 BRAM。这样既省去 IP 迁移,也让综合结果更容易控制。
综合选项的差异主要集中在默认行为上。ISE 的 XST 对未初始化寄存器比较宽容,综合出来是 FDR 初值 0;Vivado 对寄存器默认初值更敏感,如果你依赖了上电初值,最好在代码里写明复位逻辑,而不是靠综合器猜。Vivado 里可以打开-verbose看 warning,凡是出现latch或incomplete assignment的都要盯紧,这些在 ISE 里可能就是一笔带过的 warning,在 Vivado 里可能直接造成功能不对。
5. FPGA 俄罗斯方块避坑指南:五类让人想砸板子的问题
把五个版本来回对照看,真正的血泪经验都在下面这几类问题上。每一条我都按现象、原因、解决的顺序写,你出问题时按葫芦画瓢就能找到位置。
5.1 现象:屏幕斜条纹或整体滚动,画面像没锁相
现象是 VGA 显示器显示出来的画面整体向左或向右滚动,像老电视没调好台一样,或者画面斜切,一个完整画面被分成两半错开。
原因是同步时序没有严格遵守参数。最常见的是像素时钟与同步信号周期不匹配,比如用了 50MHz 时钟却按 25MHz 的周期算计数器,或者 hsync 的有效区间算错一位。VGA 标准里 640x480@60Hz 的 hsync 是负极性,也就是说空闲时为高、同步脉冲为低,如果你把极性写反,很多显示器也能自适应,但部分显示器就会滚动。
解决方法是先确认 clk_pix 频率,再对照 800x525 的总周期逐项核对。可以用 ILA 或者示波器看 hsync 的周期,应该是 31.77us,vsync 周期应该是 16.68ms。不想上示波器的话,用仿真把计数器波形抓出来,数一下是不是 800 和 525 两个数字。
5.2 现象:方块旋转后瞬移或者直接嵌进墙里
现象是按旋转键之后,方块位置突然跳到旁边几格,或者旋转后的形状和你按下去之前的预期完全不一样,更严重的是方块穿进了已有堆叠区域里。
原因是旋转后坐标直接覆盖了当前坐标,没有做碰撞试探。旋转只是把四个相对坐标换了,但方块左上角的参考点没变,贴墙时旋转后的形状可能就会超出边界。如果你先做碰撞再更新坐标,这个问题根本不会出现。
解决方法是把旋转拆成三步:算出旋转后的临时坐标、在原有位置做碰撞检测、碰了就把 cur_x 左右偏移 1 到 2 格再试。三步都失败了才认为旋转无效。这个流程在五个版本里 V3 以上都实现了,你可以直接抄 V3 的旋转模块。
5.3 现象:按键连发、一碰就停:消抖和沿检测的账
现象是按键按一次,方块移动了好几次,或者按键响应迟钝,要按住很久才动一次。
原因是按键信号没经过同步器和消抖,或者消抖之后没有做边沿检测。机械按键的抖动时间通常有 5-20ms,在这个区间内信号会反复跳变。如果游戏逻辑每个时钟周期都采样按键,一次按下会被当成几十次按下。
解决方法是三段式:先用两个触发器同步按键到时钟域,再用计数器消抖,最后打一拍取上升沿。取沿的表达式是按下有效的高电平信号与它延迟一拍的信号做与操作。
// 边沿检测:btn_stable 是消抖后的按键电平 reg btn_stable_d; always @(posedge clk) begin btn_stable_d <= btn_stable; end wire btn_press = btn_stable & ~btn_stable_d;这一段接好之后,按键按下只产生一个周期的btn_press脉冲。后面再接“长按时重复移动”的逻辑时,要单独用另一个计数器控制重复频率,不能把采样脉冲直接接到移动信号上。
5.4 现象:仿真和上板表现完全两样:复位与时钟的坑
现象是仿真里所有功能都正常,下载到板卡上之后,要么卡住不动,要么随机乱走,按键完全不响应。
原因是复位信号没同步。很多人习惯用按键直接给rst_n打低电平,但这个异步复位信号没有经过时钟同步,释放时如果刚好落在时钟沿附近,寄存器可能进入亚稳态。上电瞬间表现随机,时好时坏,这是最著名的 FPGA 玄学问题之一。
解决方法是做一个复位同步器:外部复位信号先打两拍,再送到所有模块,同时内部电路里再对复位释放做一次握手。V3 版本里有个reset_sync模块,直接复用即可。
另一个常见原因是时钟没起。如果你用了 MMCM/PLL,锁相环的 locked 信号没接入复位逻辑,时钟还没稳定寄存器就开始跑,同样会出现不可复现的乱象。解决办法是把 locked 信号和复位信号做与逻辑,locked 为低时保持复位。
5.5 现象:Vivado 综合 VHDL 老代码报各种类型错误
现象是同一份 VHDL 代码在 ISE 里能综合通过,放到 Vivado 里报一堆port type mismatch或者index constraint violation。
原因是 ISE 默认对 VHDL-93 的宽容度更高,很多老代码用了ieee.std_logic_arith和ieee.std_logic_unsigned这些非标准库,Vivado 默认对库的检查更严格,推荐使用numeric_std。另一个是 port 类型问题,ISE 里可以用buffer端口,Vivado 里容易在综合时报错,推荐把内部信号引出改成out加一个内部连线。
解决方法是先统一改用ieee.numeric_std,把std_logic_vector的加减法明确改成unsigned或signed后再运算。端口上的buffer全部改掉,内部信号单独命名为xxx_int,再用assign xxx_out = xxx_int输出。这样改完的代码在 ISE 和 Vivado 里都能过综合,V4 版本刻意保留了一处这种对照写法,打开那个模块看注释就能明白差异在哪。
6. 把五版工程吃透之后再做的三件事:双人对战、ILA 回放与版本化改造
五套工程都跑通之后,继续往里加功能的方向比你想的多,我挑三个收益最大、投入也最低的。这些改动不是为了炫技,而是验证你对模块边界的理解是不是真的到位。
6.1 从单机到双人:把 game_core 例化两次,模块边界立刻现原形
双人俄罗斯方块的难点不在游戏逻辑,而在显示仲裁。把game_core例化两份,一份管左侧场地,一份管右侧场地,按键各接各的,场矩阵各自维护。真正要改的是 VGA 绘制部分:原来一个field画全区,现在要根据显示坐标判断当前像素属于左屏还是右屏,再选择对应的场矩阵。
如果你前面把模块接口理清了,这个改动只需要改顶层和 VGA 绘制,game_core 一行都不用动。如果发现需要动 game_core,说明模块的边界之前画歪了,这次刚好补上。
6.2 用 ILA 抓住一场对局:把黑匣子重新变成白盒
下载到板卡上之后,内部信号就成了黑匣子。Vivado 里的 ILA 可以抓内部信号,抓什么路径写什么信号,比如cur_x、cur_y、collide、fall_tick,再加几个场矩阵的 bit。设置成 1024 深度,抓满一场五分钟的对局没有问题。
抓完把数据导出成 CSV,拿脚本把它还原成 ASCII 画布,一帧一帧看回放。这个方法最狠的地方在于:任何“偶现”的 bug,只要你抓过一次波形,就能定位到是哪一拍出了错。我之前遇到过一个下落偶发双格的 bug,用回放看到是fall_tick跨时钟域多采样了一次,跟直觉猜的方向完全不一样。
我的习惯是每改一次逻辑,先做一次回放对比,再做新的对局测试。版本多不是让你每版都从头打一遍,而是给你一个对比基线:V3 改坏了能回退,V4 的 VHDL 行为不对能对着 V3 的波形逐拍核对。五套工程摆在一起,最大的价值就是让你知道每一步改动到底改出了什么。希望帮到你。
本文还有配套的精品资源,点击获取