
简介涵盖5个版本的FPGA俄罗斯方块完整工程面向数字系统设计学习者与FPGA开发者可用于掌握硬件描述语言、EDA工具流程及游戏逻辑在可编程逻辑器件上的实现方法。压缩包共405个文件、18.15MB包含Verilog(.v)与VHDL(.vhd)源码以及Vivado/ISE等配套工程文件.xpr/.xise/.qpf/.qsf还有VGA显示、PS2键盘等模块设计并提供仿真波形、约束文件、生成比特流及综合实现报告等便于直接打开工程进行仿真、综合与上板验证。不同版本覆盖Vivado与ISE两代工具链也有两种语言实现方便对比语法风格和工具差异资源中还包括综合/布局布线报告、管脚约束和下载配置文件能完整还原从设计到落地的流程。目前已有3063人学习适合FPGA入门至进阶者作为项目式参考资料也可作为课程设计或电子竞赛的原型方案。 前几天整理硬盘翻出自己攒了很久的FPGA俄罗斯方块工程居然凑齐了5个完整版本。当时做这个项目纯粹是为了练手——想把Verilog和VHDL都过一遍又不想只用仿真软件空跑于是就在Vivado和ISE两种工具链上分别建了工程慢慢跑通了显示、按键、消行这些完整逻辑。如果你也想在FPGA开发板上做一个能玩的游戏或者想系统地对比学习两种硬件描述语言、两代Xilinx工具链的差异这套工程是个很合适的参考。俄罗斯方块看起来简单但在FPGA上实现它其实覆盖了数字逻辑设计的不少核心知识点模块划分、状态机设计、时序控制、信号同步、显示驱动。而且做完之后你会对“硬件描述语言到底怎么描述一块电路”有更直观的理解而不只是停留在仿真波形层面。下面我会把整套工程的架构思路、两种语言的写法差异、Vivado和ISE的使用注意点、以及我踩过的坑都拆开讲一遍。1. 整体设计与版本划分思路1.1 为什么一口气做了5个版本很多人在FPGA上做俄罗斯方块基本就是写一套Verilog跑通Vivado就完事。我之所以凑出5个版本是因为当时手里有几块不同型号的板子工具链也不一样干脆把组合都做全了版本语言目标工具适用板卡方向V1VerilogVivadoArtix-7、Kintex-7等较新器件V2VHDLVivado同上主要给VHDL学习者参考V3VerilogISESpartan-6、老款开发板V4VHDLISE老平台VHDL的组合V5Verilog简化版Vivado入门教学用代码量最少、注释最全5个版本的核心游戏逻辑是一样的区别在于语言风格、工具适配、代码组织方式。这样做的直接好处是无论你是新手入门还是手头只有老开发板都能找到能直接跑的工程。另外两套语言各有各的“脾气”用同一个游戏逻辑做载体来对比比看书本语法印象深得多。1.2 系统模块划分从“贪吃蛇式”思路到标准架构俄罗斯方块的显示逻辑和贪吃蛇类似都是在一个固定网格里不断刷新——但俄罗斯方块多了“方块旋转”和“消行”机制所以整体架构要更严谨一些。我最终把整个设计拆成了5个核心模块顶层控制器负责总的复位、暂停、游戏状态切换。游戏逻辑模块包含方块生成、下落计时、碰撞检测、消行判断、分数累计。显示驱动模块把20×10的游戏区域映射到VGA 640×480的画面。按键输入模块去抖、边沿检测、方向与旋转命令解析。时钟分频模块从板载时钟分出像素时钟和下落节拍时钟。这种划分方式不是拍脑袋定的。FPGA设计的核心思维就是“谁负责什么”要想清楚。游戏逻辑如果和显示驱动混在一起综合出来的电路时序容易乱改一个功能要动一大片代码。模块化之后显示驱动可以单独仿真游戏逻辑也可以用独立testbench压测。1.3 显示方案选型为什么不走HDMI做游戏界面常见选择有VGA、HDMI、OLED/LCD屏。对于俄罗斯方块这类低分辨率游戏VGA是最划算的方案。原因有三点VGA时序协议公开且简单640×48060Hz这种基础分辨率在FPGA上完全是“小场面”大多数开发板自带VGA接口插个显示器就行VGA引脚少约束写起来也省事。HDMI虽然画质更好、接线简单但需要处理TMDS编码和高速差分信号在入门级FPGA上还需要专门的IP核很容易把游戏逻辑的重点带偏。OLED/LCD这类方案则受限于屏幕尺寸显示的信息量不够。所以整套工程全部采用VGA输出这也是目前社区里大多数FPGA游戏项目的常见做法。2. Verilog与VHDL的工程实现对照2.1 两套语言写同一个模块差异有多大很多初学者纠结第一个该学Verilog还是VHDL。我的经验是如果你以后想找工作、看别人的开源代码先学Verilog因为Verilog的生态最丰富绝大多数FPGA开源项目都是用Verilog写的如果你在学校上《数字电路》这类课、或者在欧洲企业做项目VHDL的比例会更高它的强类型检查在大型工程里更“稳”。同一个“消行判断”逻辑我用两套语言各写了一遍直观感受一下区别。Verilog版本// 扫描10行检查是否全满 always (posedge clk or posedge rst) begin if (rst) begin lines_cleared 0; end else if (clear_en) begin for (int row 0; row 20; row row 1) begin if (grid[row][0] grid[row][1] ... grid[row][9]) begin // 该行已满执行消行 shift_rows(row); lines_cleared lines_cleared 1; end end end endVHDL版本process(clk, rst) begin if rst 1 then lines_cleared (others 0); elsif rising_edge(clk) then if clear_en 1 then for row in 0 to 19 loop if grid(row) 1111111111 then shift_rows(row); lines_cleared lines_cleared 1; end if; end loop; end if; end if; end process;可以看到VHDL对数组和位向量的处理更“结构化”——grid(row) 1111111111里10个1的字面量写法在可读性上比Verilog那串grid[row][0] grid[row][1]...要清爽一些而Verilog在循环、位选、三态门等地方写法更灵活代码更短。两者没有绝对优劣但在不同场景下各有擅长。2.2 工程文件组织结构为了让5个版本互不干扰、又能方便对照我把所有工程按目录分开每个目录里再按语言和源码/约束/仿真做三层划分。推荐你也这样组织tetris_fpga/ ├── V1_Verilog_Vivado/ │ ├── rtl/ # 所有.v源码 │ ├── xdc/ # Vivado约束文件 │ └── sim/ # testbench ├── V2_VHDL_Vivado/ │ ├── rtl/ # 所有.vhd源码 │ ├── xdc/ │ └── sim/ └── ...其余版本同理这样做的原因是不同工具的工程文件格式不兼容Vivado用.xprISE用.xise如果强行走同一个目录打开工程时会经常报路径错误分开之后反而清爽还能在Git里单独追踪每个版本的变更。2.3 综合结果对比我在同一台电脑上分别用Vivado 2018.3和ISE 14.7做过综合。Vivado对Verilog和VHDL都做了优化占用资源差别不大Artix-7上大约消耗400多个LUT、200多个寄存器、1个MMCMISE在Spartan-6上综合出来的结果也类似但VHDL版本偶尔会多出几个LUT原因是VHDL的强类型转换会引入额外的冗余逻辑。在入门级游戏项目里这点资源差可以忽略不计。3. 核心逻辑实现与实操要点3.1 方块数据编码用一个三维数组描述7种形状俄罗斯方块有7种基础形状我用了一个三维数组存储它们的初始形态。每个方块用一个4×4的网格表示有填充的位置写1。Verilog里这样写// tetromino_types[7][4][4] localparam [0:6] [0:3] [0:3] TETRIS_SHAPES { // I {4b0000, 4b1111, 4b0000, 4b0000}, // O {4b0110, 4b0110, 4b0000, 4b0000}, // T {4b0100, 4b1110, 4b0000, 4b0000}, // S {4b0110, 4b1100, 4b0000, 4b0000}, // Z {4b1100, 4b0110, 4b0000, 4b0000}, // J {4b1000, 4b1110, 4b0000, 4b0000}, // L {4b0010, 4b1110, 4b0000, 4b0000} };旋转操作不直接修改数组数据而是在显示和碰撞检测时统一做坐标变换。这样好处很明显原始数据永远不会被改坏调试旋转逻辑时只要盯住坐标公式即可。旋转90度本质上就是坐标映射——(x, y)变成(y, 3-x)逆时针这个变换在两种语言里都能用一行循环实现。3.2 碰撞检测提前“试一试”碰撞检测是俄罗斯方块最容易写出隐藏Bug的地方。我的做法是在所有移动和旋转操作执行前先做一个“模拟放置”把当前方块按照目标位置计算一遍所有有效格子的坐标检查这些坐标是否超出游戏区域边界左右墙、底部再检查这些坐标是否与已经固定的方格重叠全部通过才允许执行移动/旋转。这个“先试后动”的思路本质上就是数字逻辑设计里的“预定检查”思想。有人习惯在操作执行后再回滚但FPGA里做回滚需要保存现场很浪费寄存器。提前算一遍只需要一组组合逻辑加几个比较器电路简单得多。3.3 消行与下落状态机的艺术整个游戏流程我用一个状态机控制IDLE - SPAWN - FALLING - CHECK_LINES - LOCKED - GAME_OVERSPAWN生成新方块判断是否与已有格子重叠是则游戏结束FALLING方块按节拍下落碰到障碍物或底部则进入LOCKEDCHECK_LINES扫描20行把满行消掉上方的行整体下移LOCKED把方块数据写入网格然后回到SPAWN。消行时“上方行整体下移”在Verilog里写起来有点绕本质是把2D网格中指定行以上的数据整体搬移。VHDL里可以用数组切片和循环完成但仿真时要留意时序——这段代码很容易产生锁存器Latch。我建议在写这段逻辑时时刻检查所有分支都必须给变量赋值否则综合工具会推断出意想不到的锁存器。3.4 VGA时序驱动640×48060Hz为例VGA驱动是这个项目里跟“真实世界”打交道的部分也是我最建议你花时间独立做一遍的模块。VGA协议本身不复杂核心是四个关键信号行同步HSYNC、场同步VSYNC、以及RGB三通道数据。以640×48060Hz为例像素时钟需要25.175 MHz我一般直接用25 MHz实际显示上肉眼看不出差别。整行800个像素时钟其中640个有效显示、96个同步脉冲、48个后沿、16个前沿整个画面525行其中480行有效显示、2行同步脉冲、33行后沿、10行前沿。代码里用两个计数器就可以完成同步信号生成// 行计数器 always (posedge pclk or posedge rst) begin if (rst) h_count 0; else if (h_count 799) h_count 0; else h_count h_count 1; end // 场计数器 always (posedge pclk or posedge rst) begin if (rst) v_count 0; else if (h_count 799) begin if (v_count 524) v_count 0; else v_count v_count 1; end end同步信号的高/低电平周期就用一个判断逻辑比如行同步在h_count等于0到95之间拉低场同步在v_count等于0到1之间拉低。显示区域的位置则是h_count在144到783、v_count在35到514之间。这些参数是我从VGA时序表里查出来、然后在显示器上实测微调过的能直接复用。3.5 按键输入去抖是大事开发板上的按键大多是机械按键按下瞬间会产生数毫秒的抖动十几纳秒的边沿用示波器看会有一串毛刺。如果不对按键做去抖你会发现方块经常一次动两格甚至“跳”一下。去抖的本质很简单检测到电平变化后等10~20ms再确认一次。我用的方法是状态机计数器采样时钟20ms按键电平保持两个采样周期以上才认为有效。另外要提醒一件事不同板卡按键按下时是输出高还是低不一样做约束前一定看原理图。我的板子上按键按下接地所以拉低才是触发。4. Vivado与ISE的使用避坑4.1 新建工程与约束文件的差异Vivado和ISE虽然都是Xilinx家的工具但差异很大。ISE的发展时间早界面朴素、编译慢但对老器件Spartan-3、Spartan-6支持好Vivado则全面转向新架构界面现代化、综合速度快、对较新器件7系列及以后有完整支持。俄罗斯方块这个项目在两者上跑都没有功能问题但建议优先用Vivado除非你的板子太老只有ISE能识别它。约束文件是最大的区别。ISE用的是UCFVivado用的是XDC。同一个引脚约束UCF写法是NET clk LOC E3; NET clk IOSTANDARD LVCMOS33;换成XDC就变成set_property PACKAGE_PIN E3 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk]如果你用得比较多这个转换过程很容易出错。我的经验是在Vivado里用get_ports时端口名必须和源码里的变量名完全一致大小写都不能错而在ISE里约束名通常是信号名不太涉及语法检查写错也常常不报错直到上板才出问题。4.2 ISE在Windows 10/11上启动失败如果你跟我一样还要在老电脑上跑ISE大概率会遇到双击启动图标“闪退”的情况。这是ISE老掉牙的代码跟新版Windows不兼容的经典问题。我的解决办法是用管理员权限启动ISE在安装目录找到settings64.bat切换到命令行手动启动若还不行修改Windows兼容性设置为Windows 7。每次跑ISE综合俄罗斯方块工程我保守估计比Vivado慢三倍以上但Spartan-6这种老芯片确实只有它认识慢性子也得忍。4.3 仿真技巧不要直接上板调试FPGA上板调试需要反复烧写比特流一次10分钟效率极低。我强烈建议先做仿真。俄罗斯方块工程里我只用Vivado自带的仿真器xsim就能验证大部分逻辑。关键是把testbench写“狠一点”例如直接初始化一个已经快满的网格验证消行功能或者强迫方块贴墙旋转测试边界检测。如果你习惯用第三方仿真器比如ModelSim/Questa注意Vivado的xsim和ModelSim在解析VHDL时不完全一致ModelSim遇到std_logic_vector和integer混用的问题会更严格所以我通常直接用各工具集成的仿真器来跑对应语言避免无谓的兼容性折腾。5. 常见问题与独门避坑记录5.1 信号同步问题导致方块乱跳第一次上板时我遇到过方块偶尔“瞬移”的情况。排查了好久才发现是按键信号没有做跨时钟域处理。我用的板载时钟经过MMCM分频后与按键扫描逻辑不在同一个时钟域直接连在一起会产生亚稳态。解决方式是在按键进入游戏逻辑前用两级触发器做同步reg key_sync1, key_sync2; always (posedge clk) begin key_sync1 key_in; key_sync2 key_sync1; end这是一条FPGA开发的“铁律”跨时钟域的信号必须打两拍否则仿真可能正常、上板就抽风。5.2 显示区域偏斜或闪烁如果屏幕显示出来“歪了”或者画面有轻微滚动多半是VGA时序参数不对。先确认像素时钟频率是否准确再检查HSYNC和VSYNC的前沿、后沿参数。不同显示器的容忍度不太一样有些面板对行同步参数非常敏感。我开始使用24.75 MHz的分频配置时画面明显不稳换成25 MHz之后恢复正常。遇到花屏或偏屏优先怀疑这两点而不是去改游戏逻辑。5.3 Vivado综合报“Latch inferred”Vivado在综合时如果在某个always块里发现所有分支没有覆盖所有赋值情况会报“Latch inferred”警告。俄罗斯方块的按键去抖和旋转模块很容易触发这个问题。排查方法很直接检查每个if-else是否有对应的else分支case语句是否有default分支。VHDL那边相对好一些因为强类型语言不允许信号在某些分支下不赋值编译阶段就会拦截。提示综合出的锁存器在仿真里可能“表现正常”但上板后因为时序收敛问题会产生极难查的Bug。所有警告里Latch inferred的优先级最高见到必查。5.4 上板后方块下落过快如果你按我的方案做下落节拍由一个分频计数器控制注意把计数值设置得大一些。比较常见的坑是仿真时用10ms搞定一个下落周期感觉正好但显示器实际刷新率是60Hz画面在两次刷新之间会“卡顿”。我的建议是初始下落节拍设置在500ms到1s之间后续随着分数提高逐步加快这个手感最接近经典俄罗斯方块。6. 从这套工程里能学到什么最后再分享一点个人体会。这个俄罗斯方块项目看似只是一个“玩”的项目但如果你把它吃透收获会远远超出“写一个游戏”本身。你会熟悉FPGA的模块化设计流程理解状态机在控制类逻辑里的核心地位掌握VGA这类常用接口的时序协议还会在不断的仿真和上板调试中建立硬件直觉——什么是亚稳态、什么是锁存器、什么是时序收敛。这些能力在后续做图像处理、接口通信、嵌入式软核开发时都是通用的。如果你准备自己动手做一遍我的建议是先跑V5简化版用Vivado打开工程看懂代码结构然后把VGA显示模块单独拎出来玩一玩改改分辨率和颜色等V5跑通了再去对比V1的完整版看看多了哪些逻辑如果你对VHDL感兴趣把V2版本打开对照着读一遍两套语言的差异会给你留下特别深的印象。至于ISE的版本留着备用就好——万一哪天你淘到一块Spartan-6的老板子直接就能用上。本文还有配套的精品资源点击获取