
简介这份文档是燕山大学 EDA 课程设计「游戏机」项目的完整设计报告面向电子、通信、自动化等专业修读数字电子技术、数字逻辑电路课程的学生。设计要求用三位数码管循环显示 07按键控制启停三管数字相同即为获胜并附加三路循环速度不同、停止延时不同、胜出时花样显示或蜂鸣提示等条件。报告按总体设计与模块解析展开涵盖分频、延时、计数、扫描、比较、蜂鸣及 LED 显示七个模块涉及 74160、74161、74153、7448、7485 等芯片以及 T 触发器、D 触发器的连接与仿真验证配有各模块电路图和仿真结果。资源包共 1 个 docx 文件约 1.06MB图文一体读者可对照赛题要求复现分频、扫描与比较逻辑也可将其作为课设选题参考与报告撰写模板。目前已有 209 人学习下载。1. 燕山大学EDA课程设计游戏机把一份 docx 变成能跑的东西每年课设周一开始班群里都会转一份标题带「整理」的文档打开是十几页功能要求和截图真正落到手上的只有一句话用 EDA 技术实现一款简易游戏机。卡住人的从来不是这句要求而是后面三件具体的事——怎么用 Verilog 把游戏规则描述成可综合的状态机怎么把 50 MHz 板载时钟变成 VGA 要的像素节拍以及怎么把按键、显示接口用嘉立创EDA 画成一块能焊的底板。这套东西的读者很明确自动化、电子信息、计算机方向做 EDA 课程设计的本科生以及实验室里带课设的助教。选贪吃蛇、俄罗斯方块还是打砖块差别不大底层是同一套结构——像素坐标、游戏节拍、碰撞检测、按键消抖。把这一套走通换玩法只是重写状态机的转移条件。下面按建模、仿真上板、画板、联调的顺序展开。代码都是能直接抄进工程的最小量级参数会给出取值理由综合报错和花屏死机的定位顺序也一并写上。2. 用 Verilog 给 EDA 课程设计游戏机建模像素坐标、蛇身存储与状态机2.1 640×480 行场计数把游戏坐标钉在 VGA 扫描线上显示这块最容易翻车的地方是「游戏坐标」和「扫描坐标」两套体系没对齐。VGA 的行场计数器一直在跑它数的是扫描线游戏逻辑关心的是 20×15 的格子。中间要靠一次除法和一次比较把两者接上接错了就是整屏偏移、行丢失或者画面滚动。先把 640×48060 Hz 的时序参数定死。这几个数字是标准值改任何一个都会让显示器认不出信号。像素时钟理论值是 25.175 MHz实际用 50 MHz 晶振二分频出 25 MHz绝大多数显示器都能锁住。参数数值说明像素时钟25 MHz由板载 50 MHz 经 PLL 二分频得到行总数800640 可见 16 前肩 96 同步 48 后肩场总数525480 可见 10 前肩 2 同步 33 后肩网格边长32 px640/32 20 列480/32 15 行逻辑网格20×15 300 格蛇身建议不超过 60 节留出环形缓冲余量// 25 MHz 像素时钟下的 640x48060 Hz 行场计数器 localparam H_VISIBLE 640, H_FRONT 16, H_SYNC 96, H_BACK 48; localparam V_VISIBLE 480, V_FRONT 10, V_SYNC 2, V_BACK 33; localparam H_TOTAL H_VISIBLE H_FRONT H_SYNC H_BACK; // 800 localparam V_TOTAL V_VISIBLE V_FRONT V_SYNC V_BACK; // 525 reg [9:0] h_cnt; // 0..799 reg [9:0] v_cnt; // 0..524 always (posedge pix_clk or negedge rst_n) begin if (!rst_n) begin h_cnt 10d0; v_cnt 10d0; end else if (h_cnt H_TOTAL - 1) begin h_cnt 10d0; v_cnt (v_cnt V_TOTAL - 1) ? 10d0 : v_cnt 10d1; end else begin h_cnt h_cnt 10d1; end end // 同步信号低有效极性反了会直接黑屏 assign vga_hs ~((h_cnt H_VISIBLE H_FRONT) (h_cnt H_VISIBLE H_FRONT H_SYNC)); assign vga_vs ~((v_cnt V_VISIBLE V_FRONT) (v_cnt V_VISIBLE V_FRONT V_SYNC)); // 只有落在可见区才允许输出颜色 wire video_on (h_cnt H_VISIBLE) (v_cnt V_VISIBLE);逻辑说明行计数器满 800 归零场计数器满 525 归零这个套嵌结构保证了行场严格同步。同步脉冲用区间比较生成写成低有效是因为绝大多数 VGA 显示器按负极性同步工作。video_on是关键信号消隐期如果还输出颜色画面边缘会出现彩条。像素到格子的换算不要用除法直接在位宽上做文章h_cnt[9:5]等价于h_cnt / 32综合出来就是几根线。列号px h_cnt[9:5]范围 0~19行号py v_cnt[8:5]范围 0~14格号cell_id px * 15 py落在 0~299。这三个量在后面画蛇、画食物、查位图时反复用到。2.2 蛇身存储坐标数组加整体移位比环形缓冲好懂也好调蛇身怎么存直接影响后面自撞判定的写法。常见的三种做法坐标数组、300 位占用位图、环形缓冲区。环形缓冲区省资源但指针容易写错占用位图查得快但删尾节麻烦。课设这个规模下我一般直接上坐标数组逻辑最直白综合面积也完全吃得消。约定sx[0]/sy[0]永远是蛇头下标越大越靠近蛇尾每个游戏节拍整体向后挪一格。这样自撞判定就是拿「下一格」去和sx[0..body_len-1]逐项比较不需要任何指针运算。// ---- 蛇身64 项坐标数组sx[0]/sy[0] 为蛇头 ---- reg [4:0] sx [0:63]; // 列坐标 0..19 reg [3:0] sy [0:63]; // 行坐标 0..14 reg [5:0] body_len; // 当前长度同时就是分数 reg [1:0] dir; // 00 上 / 01 下 / 10 左 / 11 右 reg game_over; // 8 Hz 游戏节拍25_000_000 / 8 3_125_000 localparam TICK_DIV 25_000_000 / 8; reg [21:0] tick_cnt; wire tick (tick_cnt TICK_DIV - 1); always (posedge pix_clk or negedge rst_n) if (!rst_n) tick_cnt 22d0; else tick_cnt tick ? 22d0 : tick_cnt 22d1; // 下一格坐标。用 6 位无符号承载负偏移63 就是 -1 的位模式 wire [5:0] tgt_x {1b0, sx[0]} ((dir 2b10) ? 6d63 : (dir 2b11) ? 6d1 : 6d0); wire [5:0] tgt_y {1b0, sy[0]} ((dir 2b00) ? 6d63 : (dir 2b01) ? 6d1 : 6d0); wire hit_wall (tgt_x 6d19) || (tgt_y 6d14); // 自撞并行扫描有效蛇身 integer i; reg hit_self; always (*) begin hit_self 1b0; for (i 0; i 63; i i 1) if (i body_len sx[i] tgt_x[4:0] sy[i] tgt_y[3:0]) hit_self 1b1; end // 食物位置9 位 LFSR 自由滚动吃到时采样一格 reg [8:0] lfsr; wire [8:0] lfsr_next {lfsr[7:0], lfsr[8] ^ lfsr[4]}; // x^9 x^4 1 wire [8:0] food_cell lfsr_next % 9d300; // 映射到 0..299 wire [4:0] food_x food_cell / 9d15; // 0..19 wire [3:0] food_y food_cell % 9d15; // 0..14 wire eat_food (tgt_x[4:0] food_x) (tgt_y[3:0] food_y); always (posedge pix_clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i 64; i i 1) begin sx[i] 5d10 - i[4:0]; // i0/1/2 给出 10/9/8构成初始三节 sy[i] 4d7; end body_len 6d3; dir 2b11; game_over 1b0; lfsr 9d1; // LFSR 种子不能为 0 end else if (tick !game_over) begin lfsr lfsr_next; if (hit_wall || hit_self) begin game_over 1b1; // 冻结画面等复位 end else begin for (i 63; i 0; i i - 1) begin sx[i] sx[i-1]; // 整体后移一格 sy[i] sy[i-1]; end sx[0] tgt_x[4:0]; sy[0] tgt_y[3:0]; if (eat_food) body_len body_len 6d1; // 吃到食物长度加一尾巴不丢 end end end逻辑说明tgt_x/tgt_y用 6 位宽来算是为了让「向上越界」这种下溢情况能被检测到。如果只写sx[0] - 1并用 5 位承接0 减 1 会回绕成 19蛇会从对边穿出来这是课设里最常见的一类莫名其妙 bug。参数说明TICK_DIV决定蛇的速度8 Hz 是新手能反应过来的档位想调难度只改这一个常数。body_len直接当分数用接数码管时先转成 BCD 再送显示。lfsr用 9 位是因为 2^9-1 511对 300 取模后落在合法格号内同时避免食物刷在蛇身上的概率过高。2.3 按键消抖与方向锁存4 个必须定下来的参数按键是异步输入直接进状态机会引入亚稳态机械触点还会抖 5~10 ms不消抖就会出现一次按下被识别成好几次蛇当场自撞。标准做法是两级同步加计数确认四级按键各配一个计数器。// 四路按键消抖25 MHz 下计数到 500_000 正好 20 ms localparam DEB_MAX 500_000; reg [18:0] deb_cnt [0:3]; reg [3:0] key_sync1, key_sync2, key_stable; integer k; always (posedge pix_clk or negedge rst_n) begin if (!rst_n) begin key_sync1 4b1111; key_sync2 4b1111; key_stable 4b1111; for (k 0; k 4; k k 1) deb_cnt[k] 19d0; end else begin key_sync1 key_pin; // 第一级把异步输入收进像素时钟域 key_sync2 key_sync1; // 第二级压掉亚稳态 for (k 0; k 4; k k 1) begin if (key_sync2[k] ! key_stable[k]) begin if (deb_cnt[k] DEB_MAX - 1) begin deb_cnt[k] 19d0; key_stable[k] key_sync2[k]; end else deb_cnt[k] deb_cnt[k] 19d1; end else deb_cnt[k] 19d0; // 电平一致就清计数防止累积误判 end end end // 方向锁存只在游戏节拍边沿更新并禁止 180 度回头 wire [1:0] dir_req ~key_stable[0] ? 2b00 : ~key_stable[1] ? 2b01 : ~key_stable[2] ? 2b10 : ~key_stable[3] ? 2b11 : dir; wire opposite ((dir_req 2b00) (dir 2b01)) || ((dir_req 2b01) (dir 2b00)) || ((dir_req 2b10) (dir 2b11)) || ((dir_req 2b11) (dir 2b10)); always (posedge pix_clk or negedge rst_n) if (!rst_n) dir 2b11; else if (tick !opposite) dir dir_req;逻辑说明key_stable只在按键连续保持 20 ms 后才翻转抖动期间计数被清零。方向锁存挂在tick上而不是像素时钟上是为了保证一个游戏节拍内最多接受一次转向指令否则玩家手指快一点同一拍内连发两次蛇会在还没移动的情况下被判成自撞。参数取值为什么是这个值消抖计数上限500_00025 MHz × 20 ms覆盖机械按键 5~10 ms 的全部抖动窗口同步级数2 级单级触发器的 MTBF 太低两级后亚稳态概率可忽略方向锁存时刻8 Hz 节拍边沿防止一拍内连转两次导致自撞判定错乱游戏节拍8 Hz反应窗口约 125 ms改成 16 Hz 就是挑战模式3. 仿真与上板把 EDA 课程设计游戏机从代码推到开发板3.1 工程建立与顶层端口约定Quartus Prime Lite 建工程时器件选型要跟实验室板子对上Cyclone IV E 系列里 EP4CE6 和 EP4CE10 最常见选错器件后面引脚分配会全废。顶层模块只暴露板级信号内部 PLL、计数器、状态机全部封装进去这样仿真时只需要驱动 6 个输入。module snake_top ( input clk_50m, // 板载 50 MHz 有源晶振 input rst_n, // 复位按键低有效 input [3:0] key_pin, // 上/下/左/右按下为低 output vga_hs, output vga_vs, output [2:0] vga_r, // 每路一位色深串 270 欧进 VGA 接口 output [2:0] vga_g, output [2:0] vga_b, output [7:0] seg, // 数码管段选显示分数 output [3:0] seg_sel // 数码管位选 ); wire pix_clk, locked; pll_25m u_pll ( .inclk0 (clk_50m), .c0 (pix_clk), // 25 MHz .locked (locked) ); // 其余逻辑统一放在 pix_clk 域复位由 rst_n 与 locked 相与 wire rst_sync rst_n locked; // ... 行场计数、蛇身、消抖等模块例化 endmodule端口约定一旦定下来就不要改VGA 三路颜色各用 3 位是最低成本的方案256 色没必要课设评分看的是画面稳定和玩法完整。端口方向电平标准硬件连接要点clk_50min3.3-V LVTTL接 PLL 输入不要直接进逻辑key_pin[3:0]in3.3-V LVTTL开启内部弱上拉按键另一端接地vga_hs / vga_vsout3.3-V LVTTL直连 VGA 接口 13、14 脚vga_r/g/bout3.3-V LVTTL三路各串 270 欧电阻做简易 DACseg / seg_selout3.3-V LVTTL按板上共阴或共阳调整极性3.2 引脚分配与 25 MHz 像素时钟为什么不用计数器分频用计数器把 50 MHz 分成 25 MHz 写起来简单但分出来的时钟是普通逻辑信号走的是通用布线资源抖动大、还容易被综合器整合掉。正确做法是用 PLL IP输出直接进全局时钟网络同时它的locked信号可以当作逻辑的复位释放条件PLL 没锁定时整个画面停住而不是乱跑。# 在 Quartus Prime 的 Tcl Console 里逐段执行 # 引脚号务必替换成自己开发板原理图上的实际编号这里是占位示例 set_location_assignment PIN_E1 -to clk_50m set_location_assignment PIN_M15 -to rst_n set_location_assignment PIN_M16 -to key_pin[0] set_location_assignment PIN_N15 -to key_pin[1] set_location_assignment PIN_N16 -to key_pin[2] set_location_assignment PIN_P15 -to key_pin[3] set_location_assignment PIN_B13 -to vga_hs set_location_assignment PIN_B14 -to vga_vs set_location_assignment PIN_A13 -to vga_r[0] set_location_assignment PIN_A14 -to vga_r[1] set_location_assignment PIN_A15 -to vga_r[2] set_location_assignment PIN_B16 -to vga_g[0] set_location_assignment PIN_C13 -to vga_g[1] set_location_assignment PIN_C14 -to vga_g[2] set_location_assignment PIN_D14 -to vga_b[0] set_location_assignment PIN_E14 -to vga_b[1] set_location_assignment PIN_F14 -to vga_b[2] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to *引脚号从板子原理图上一一对照填入填完在 Pin Planner 里检查一遍有没有重复占用。电平标准统一设成 3.3-V LVTTL混用别的标准可能出现上电即发热的情况。分配完记得跑一次全编译让 Fitter 重新布局布线。3.3 用 ModelSim 写一条激励先在跑板前验掉第一次撞墙上板调试的成本远高于仿真。写一个最小 testbench把 8 Hz 节拍压缩观察重点看三件事撞墙后game_over有没有拉高、方向锁存有没有丢掉反向按键、蛇尾有没有跟着头一起挪。timescale 1ns/1ps module tb_snake_top; reg clk_50m 1b0; reg rst_n 1b0; reg [3:0] key_pin 4b1111; always #10 clk_50m ~clk_50m; // 周期 20 ns即 50 MHz snake_top dut ( .clk_50m (clk_50m), .rst_n (rst_n), .key_pin (key_pin), .vga_hs (), .vga_vs (), .vga_r (), .vga_g (), .vga_b (), .seg (), .seg_sel () ); initial begin #200 rst_n 1b1; #2_000_000; // 让蛇自己往右跑一段观察格子坐标推进 key_pin[0] 1b0; // 按下「上」 #100_000 key_pin[0] 1b1; #100_000 key_pin[1] 1b0; // 立刻按反方向的「下」应被 opposite 拦掉 #200_000 key_pin[1] 1b1; #200_000_000 $stop; end endmodule逻辑说明仿真时把波形里dut.sx[0]、dut.sy[0]、dut.dir、dut.game_over、dut.body_len加到观察窗。第一次按下「上」之后dir应该变成 2b00第二次按下「下」时dir保持不变同时蛇头坐标开始递减。如果跑出来蛇尾没动八成是移位循环的边界写反了检查for (i 63; i 0; i i - 1)里的上限是不是误写成i 0——那样会导致索引越界综合器可能直接把它优化掉仿真里表现为蛇身永远只有一节。3.4 时序约束与 SignalTap花屏和死机的排查顺序不写 SDC 也能过编译但 Timing Analysis 里所有路径都是「未约束」等于放弃了时序检查。给板载晶振打一条时钟约束PLL 派生时钟交给自动推导就够用了。# snake_game.sdc create_clock -name clk_50m -period 20.000 [get_ports clk_50m] derive_pll_clocks derive_clock_uncertainty上板后按固定顺序排比瞎试快得多。现象优先怀疑定位动作显示器提示无信号PLL 没锁定或引脚没分配示波器量 vga_hs 是否有 31.5 kHz 翻转有信号但画面滚动场总数写成 524 而非 525SignalTap 抓 v_cnt 的最大值蛇只有头在动移位循环被优化看综合报告里 sx 数组用了多少个寄存器按键偶尔连跳两下少了二级同步或消抖窗口太短抓 key_stable看有没有小于 20 ms 的毛刺报「multiple constant drivers」同一 reg 在两个 always 里赋值把位图构建和移位合并进同一个 always4. 用嘉立创EDA把游戏机做成一块能焊的底板4.1 EDA 元件引脚与焊盘未对应从网络表断点往回查把 FPGA 上的游戏机做成实物接下来就是画板。嘉立创EDA 里最让人抓狂的一类问题是原理图上连线明明都连了导出网络表一看某个器件的引脚全是悬空。原因几乎都出在符号引脚编号和封装焊盘编号没有一一对应上——这两套编号各自独立靠字符串匹配牵线对不上就断。排查按这个顺序来。第一步双击元件打开属性面板分别点「符号」和「封装」两栏的编辑把两边编号列出来对一遍。第二步在原理图上给疑似断开的引脚单独拉一根线到一个空网络用「设计管理器」看这个网络挂没挂上器件。第三步如果确实是编号不匹配改封装不要改符号——官方库里的封装被别的元件引用着改焊盘编号会连带影响其他器件。符号引脚编号封装焊盘编号网络表结果1 / 2 / 31 / 2 / 3正常连通1 / 2 / 3A / K / G引脚全部悬空PCB 上无网络1 / 2 / 31 / 1 / 2编号重复同步时报错自己画封装时焊盘编号的规则是「能不改就不改」。需要异形焊盘只调尺寸和形状编号保持和符号一致能省掉大量返工。4.2 原理图选择网络联动 PCB 的设置步骤这功能在跨页查线时省时间。步骤是固定的出问题基本都是工程结构没弄对。原理图和 PCB 放在同一个工程里而且 PCB 是由这份原理图「更新/导入变更」生成的。打开 PCB 编辑器顶部菜单进「工具 → 交叉选择」把交叉选择模式打开。回到原理图单击任意一根网络比如KEY_UPPCB 里对应的走线、焊盘应该同时高亮。反向也成立在 PCB 里点一条走线原理图上对应的网络会闪烁定位。如果只亮一边先检查工程树里原理图和 PCB 是不是同一个工程下的兄弟节点再检查 PCB 侧有没有执行过「从原理图导入变更」。常见误操作是原理图和 PCB 各改一遍网络名两边不同步之后交叉选择就失效了。规则很简单网络名的唯一来源永远是原理图PCB 只接受同步更新。4.3 按键底板布线星型接地有用但六层板在这里是浪费这块板的信号构成是按键、数码管、VGA 接口时钟最高 25 MHz速率不高。地线处理上按键回路是主要噪声源——按下瞬间的触点弹跳会在按键的局部地线上产生几十毫安的瞬态电流。所谓星型接地就是让每个按键的地各自单独拉一根线汇集到电源入口附近的一个点再统一接到主地平面避免按键电流从别的敏感回路下面穿过去在这块板上能明显减少误触发。层数典型适用这块游戏机底板要不要2 层按键、数码管、直插接口时钟低于 50 MHz够用优先选4 层有高速并行总线或开关电源不需要6 层BGA 器件、多层阻抗控制场景没必要成本翻几倍两层板的做法是顶层走信号、底层铺完整地平面按键走线不要和时钟线长距离并行。如果以后做蓝桥杯 EDA 赛项那种带高速接口的板子六层板才有意义那时候考虑的是阻抗控制和参考平面完整性跟这块按键板不是一回事。4.4 导出位号图做装配校验画完板子要出位号图交装配出之前先跑一遍自动校验能挡掉大半「焊的时候发现少个位号」的情况。从嘉立创EDA 导出 BOM 后用下面这段脚本核查位号唯一性和封装完整性。# 校验导出的 BOM/位号表位号唯一、封装非空 import csv import sys import collections path sys.argv[1] if len(sys.argv) 1 else bom.csv with open(path, encodingutf-8-sig, newline) as f: rows list(csv.DictReader(f)) # 列名按导出时的实际表头替换常见中英文两种 refs [r.get(Designator) or r.get(位号, ) for r in rows] footprints [r.get(Footprint) or r.get(封装, ) for r in rows] dup [r for r, c in collections.Counter(refs).items() if c 1 and r] empty_fp [refs[i] for i, fp in enumerate(footprints) if not fp.strip()] print(f总位号数: {len(refs)}) if dup: print(重复位号:, , .join(dup)) if empty_fp: print(缺少封装的位号:, , .join(empty_fp)) if not dup and not empty_fp: print(校验通过可以出位号图交装配)逻辑说明utf-8-sig编码是为了吃掉 Excel 导出 CSV 时带的 BOM 头不解码掉的话第一列的列名会多出不可见字符DictReader读出来全是空值。collections.Counter统计位号出现次数超过一次就是重复放置。封装列为空说明这个元件在原理图里挂了符号但没指定封装PCB 上不会有它的位置装配一定漏件。5. 联调花屏、乱跳与分数错乱的定位顺序验收前一晚的常见场景是代码编译通过、板子也烧进去了但显示器一会儿黑一会儿滚。这时候不要改代码按现象分层定位更快。先确认视频链路通不通。把示波器接vga_hs看到约 31.5 kHz 的方波就说明 PLL 和行场计数都在工作问题出在颜色或消隐逻辑上测不到波形就先查 PLL 的locked和引脚分配。行频 31.5 kHz、场频 60 Hz、场周期 16.68 ms 这三个数字记住任何一个是错的显示器都会拒绝或者滚动。# 命令行全编译加烧录验收前批量重跑比点 GUI 稳 quartus_sh --flow compile snake_game quartus_pgm -c USB-Blaster -m jtag -o p;output_files/snake_game.sof如果不想每次都开图形界面把这两条写成脚本改完localparam直接跑。现象先量哪里大概率原因显示器提示无信号vga_hs 有没有 31.5 kHzPLL 未锁定或引脚未分配有信号但画面滚动v_cnt 最大值是否为 524场总数误写成 524 而非 525蛇头走位但蛇尾不动sx[1] 有没有随 tick 变化移位循环被综合器优化掉数码管数字闪seg_sel 扫描频率低于 50 Hz 会闪高于 1 kHz 会拖影游戏速度过快TICK_DIV 常量除数是 25_000_000 / 16 而非 / 8联调时有个很实用的做法把速度节拍和消抖上限这两个localparam提到文件顶部单独一组现场演示前只改这两行。慢档用TICK_DIV 25_000_000/4方便评委看清蛇的移动轨迹快档用/16展示手感差异中间不需要动任何状态机代码。最后一类问题是分数错乱body_len在吃到食物那一拍递增但如果递增和移位写在同一拍且共用了移位后的索引计分就会多跳或漏跳。定位方式是抓body_len与sx[0]的时序关系正常情况下每次计分只进一位且与蛇头到达食物格子的那一拍严格对齐。本文还有配套的精品资源点击获取