news 2026/9/18 18:22:41

EDA课程设计游戏机全流程:Verilog RTL、嘉立创EDA载板与上板调试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
EDA课程设计游戏机全流程:Verilog RTL、嘉立创EDA载板与上板调试

简介:这份燕山大学EDA课程设计资料以游戏机为设计题目,面向电子信息、自动化等专业修读数字电子技术课程的学生,以及需要完成EDA课程设计或数字逻辑综合实验的读者。资源包内含1个docx文档,约1.06MB,为整理成型的课程设计报告,涵盖设计题目与要求、总体设计思路、各模块详细解析及仿真验证等内容。设计采用三位数码管循环显示0至7的数字,按下按钮三管滚动、抬起按钮停止,三者显示相同时判定为赢,并给出声音或LED花样提示。报告按分频、延时、计数、扫描、比较、蜂鸣、发光二极管显示七个模块展开,具体涉及74160、74161、74153、7448、7485等器件与T触发器、D触发器的配合使用,还说明了不同循环速度、不同停止延时、549进制蜂鸣计数和九级D触发器花样显示的实现方法。目前已有209人学习,可供读者参照电路结构、仿真结果与模块调试思路,完成同类课设的搭建与验证。

1. EDA 课程设计的游戏机,先想清楚要交出哪三样东西

课程设计任务书上写着「基于 EDA 技术设计一台游戏机」,很多人第一反应是找一份现成的俄罗斯方块代码,仿真跑通就交差。真正要交的东西其实是三样:一份可综合的 RTL、一块自己画的载板、一段上板能玩的演示。燕山大学这类 EDA 课程设计的评分点,基本就落在这三样能不能对齐——仿真波形再漂亮,按键一按跳两格、数码管不亮、VGA 花屏,分数就下来了。

这篇内容按一条完整落地路径走:Verilog 里把游戏的状态机、碰撞判定和视频扫描做出来,用嘉立创EDA 把按键、数码管、VGA 接口的载板画出来,再回到板上做按键消抖、时序约束和故障定位。适合正在做 EDA 课程设计的学生,也适合想把数字逻辑课上的例子真正做成一台能玩的机器的工程师。全程只用一个 50MHz 晶振当主时钟,不玩多时钟域的花活。

2. 游戏机核心的 Verilog 实现:帧节拍、碰撞判定与 VGA 扫描

课程设计里的 EDA 多数落在 FPGA 上,Cyclone IV 这类小容量器件就够。难点从来不是写游戏逻辑本身,而是把「游戏节拍」「碰撞判定」「显示输出」拆到同一个时钟域里,还要留出足够的时序余量。下面这套结构我在课程设计和答辩演示上用过很多次,改参数就能换游戏。

2.1 单时钟域下的帧节拍,比你想的更重要

新手最容易犯的错是给游戏逻辑单独分一个 60Hz 时钟,然后用always @(posedge clk_60)去驱动位置寄存器。这样器件里就多了一个时钟域,跨域信号没做同步,上板后会出现球体偶尔卡一帧、数码管乱码这类偶发问题,仿真还查不出来。

正确做法是把 50MHz 主时钟作为唯一的工作时钟,用一个单周期脉冲当「一帧」的使能信号。

// 游戏节拍:从主时钟生成单周期帧使能脉冲 module frame_tick #( parameter integer CLK_HZ = 50_000_000, // 板载晶振频率 parameter integer FRAME_HZ = 60 // 目标帧率 )( input wire clk, input wire rst_n, output reg frame_en ); localparam integer DIV = CLK_HZ / FRAME_HZ; // 50M/60 = 833333 reg [31:0] cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 32'd0; frame_en <= 1'b0; end else if (cnt == DIV - 1) begin cnt <= 32'd0; frame_en <= 1'b1; // 只高一个时钟周期 end else begin cnt <= cnt + 1'b1; frame_en <= 1'b0; end end endmodule

DIV用整数除法取整,50MHz 除以 60 得到 833333,实际帧率 60.00002Hz,人眼完全看不出。如果做贪吃蛇这类慢节奏游戏,把FRAME_HZ改成 10 或 15 会更好操作;打地鼠则适合 30 左右。frame_en只用于if (frame_en)这种使能判断,绝不能拿它当时钟用。

2.2 位置寄存器与碰撞判定:别写出组合环

游戏对象一律用坐标寄存器表示,每帧更新一次。以 640×480 的乒乓球为例,球的位置是 10 位坐标,方向用 1 位表示。

localparam integer H_ACTIVE = 640; localparam integer V_ACTIVE = 480; localparam integer BALL_SZ = 8; localparam integer PADDLE_W = 12; reg [9:0] ball_x, ball_y; reg vx, vy; // 1 = 正向, 0 = 负向 // 用当前(旧)坐标判断边界,避免组合逻辑自反馈 wire hit_x = (ball_x <= PADDLE_W) || (ball_x >= H_ACTIVE - PADDLE_W - BALL_SZ); wire hit_y = (ball_y <= 0) || (ball_y >= V_ACTIVE - BALL_SZ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin ball_x <= 10'd320; ball_y <= 10'd240; vx <= 1'b1; vy <= 1'b1; end else if (frame_en) begin if (hit_x) vx <= ~vx; // 撞左右板,反弹 if (hit_y) vy <= ~vy; // 撞上下边,反弹 ball_x <= vx ? ball_x + 1'b1 : ball_x - 1'b1; ball_y <= vy ? ball_y + 1'b1 : ball_y - 1'b1; end end

关键在于hit_xhit_y是用赋值前的旧坐标算出来的组合信号,方向翻转和坐标位移在同一帧内生效,不会出现「反弹后又往前冲一像素」的抖动。如果做打地鼠,位置来源换成 LFSR 伪随机数即可,用一个 32 位线性反馈移位寄存器的低几位当坐标,每次命中后换一次种子。

// 8 位 LFSR,取低 3 位当格子的行/列 reg [31:0] lfsr; always @(posedge clk or negedge rst_n) if (!rst_n) lfsr <= 32'hACE1_2345; else if (frame_en) lfsr <= {lfsr[30:0], lfsr[31] ^ lfsr[21] ^ lfsr[1] ^ lfsr[0]};

2.3 VGA 扫描时序与像素级图形合成

640×480@60Hz 的时序参数是固定的,把行场计数做出来,再和物体坐标比较就能直接出颜色。参数表如下:

参数数值说明
像素时钟25.175 MHz800×525×60 ≈ 25.2M
H_SYNC / H_BP / H_ACT / H_FP96 / 48 / 640 / 16合计 800
V_SYNC / V_BP / V_ACT / V_FP2 / 33 / 480 / 10合计 525
同步极性负极性同步脉冲低电平有效
localparam integer H_TOTAL = 800, V_TOTAL = 525; localparam integer H_SYNC = 96, H_BP = 48; localparam integer V_SYNC = 2, V_BP = 33; reg [9:0] h_cnt, v_cnt; reg h_act, v_act; always @(posedge clk_pix) begin // clk_pix = 25.175MHz if (h_cnt == H_TOTAL - 1) begin h_cnt <= 10'd0; v_cnt <= (v_cnt == V_TOTAL - 1) ? 10'd0 : v_cnt + 1'b1; end else h_cnt <= h_cnt + 1'b1; end assign h_act = (h_cnt >= H_SYNC + H_BP) && (h_cnt < H_SYNC + H_BP + 640); assign v_act = (v_cnt >= V_SYNC + V_BP) && (v_cnt < V_SYNC + V_BP + 480); // 有效区内的像素坐标 wire [9:0] px = h_cnt - (H_SYNC + H_BP); wire [9:0] py = v_cnt - (V_SYNC + V_BP); // 像素级合成:谁在最上层谁说了算 wire ball_area = (px >= ball_x) && (px < ball_x + BALL_SZ) && (py >= ball_y) && (py < ball_y + BALL_SZ); wire wall_area = (px < 4) || (px >= 636) || (py < 4) || (py >= 476); assign vga_r = (h_act && v_act && ball_area) ? 8'hFF : 8'h00; assign vga_g = (h_act && v_act && wall_area) ? 8'hFF : (h_act && v_act && ball_area) ? 8'h00 : 8'h00; assign vga_b = (h_act && v_act && ball_area) ? 8'h00 : (h_act && v_act && wall_area) ? 8'hFF : 8'h00;

这段合成的本质是「画家算法」:后判断的对象覆盖前面的,写的时候按球、挡板、边框、背景的顺序排?:就行。像素坐标从行场计数里减偏移得到,不要再单独维护一套计数器,减少出错面。

2.4 上板前用仿真把边界跑满

课程设计时间紧,但至少要把碰撞边界跑一遍,否则上板后球卡在角落里出不来,现场改代码很被动。

# 用 iverilog 跑一个最小激励:强制出现左边界和上边界 iverilog -g2012 -o sim.out tb_game.v game_core.v frame_tick.v vga_timing.v vvp sim.out # 输出 VCD 后可在 GTKWave 里看 ball_x / vx 的翻转时刻

Testbench 里不需要精确复现 50MHz,把CLK_HZ参数覆盖成 100 就行,几千个周期就能跑完几十帧。重点看三件事:frame_en是否每 100 个周期出现一次;vxball_x <= 12时是否翻转;h_act的占空比是否是 640/800。这三条过了,上板基本不会出现完全没图像的尴尬。

3. 用嘉立创EDA 画游戏机载板:原理图、网络联动与打样前检查

很多课程设计只做到开发板插杜邦线,演示时按键一按就复位,非常吃亏。花两天画一块载板,把按键、数码管、VGA 接口、晶振都做到一块板子上,演示稳定性完全不一样,答辩的工程完整度也上去了。嘉立创EDA 免费、库全、能直接下单打样,是这条路线里最省事的选择。

3.1 元件与封装:先解决「元件的引脚与焊盘未对应」

画板子最常见的翻车点不是布线,是符号引脚编号和封装焊盘编号对不上。自建符号时,符号里每个引脚的「编号」必须和 PCB 封装的焊盘号严格一致,名称可以随便写,编号错一位就会导致网络连到错误的焊盘上,而 DRC 是查不出来的。

元件推荐封装易错点
轻触按键 6×6SW-SMD_4P 或 THT 4 脚四脚按键常有两两内部短接,不要当独立四路用
共阴数码管0.36 寸 4 位直插共阴/共阳选错,段码全反
VGA 母座 DB15DB15-HD 直插带螺母引脚 1/2/3 是 R/G/B,13/14 是行场同步
有源晶振 50MHzSMD 5032 四脚引脚 1 是使能或悬空,别接成 GND
去耦电容 100nF0402/0603每个电源引脚旁边一颗,别共用

自建符号的正确顺序是:先确定实际买到手的器件型号和规格书,按脚位画符号,再从立创商城搜索同型号的现成封装复用;找不到就自己按规格书画焊盘,画完用「封装管理器」逐个核对编号。这一步省下的两小时,通常要用一次打样报废来还。

3.2 原理图到 PCB 的网络联动怎么设

嘉立创EDA 的原理图和 PCB 是强关联的,改完原理图必须显式同步,否则板子上的网络还是旧的。常见做法是:

  1. 原理图里执行设计 → 检查 DRC,先把单端网络、重复位号清掉;
  2. 执行设计 → 转换/更新原理图到 PCB,新建工程选「转换原理图到 PCB」,已有 PCB 选「更新 PCB」;
  3. 弹窗里勾选「保留已有布局」,这样按位号匹配,已经摆好的元件不会被重新扔到板框外;
  4. 更新完成后打开设计 → 网络面板,确认新增的网络都在,且没有出现「未连接」的红色飞线。
现象原因处理
更新后元件全跑到板框外位号被重排,无法按位号匹配撤销,先在原理图里锁定位号再更新
PCB 上多了几个孤立焊盘原理图符号引脚编号与封装焊盘号不一致回到 3.1 核对编号
网络面板里网络名带$1N网络没有命名,属于隐式连接在原理图里给关键网络手动命名
飞线全消失但走线还在原理图删了器件但没同步重新执行更新,选择删除多余器件

改完网络后飞线会自动重算,但已经布好的走线不会自动跟着动。所以布局布线之前,一定要把原理图的网络关系冻结,中途改网络等于返工。

3.3 布局顺序、去耦和地平面:星型接地什么时候才用

布局按信号流走:电源入口和稳压 → 晶振与复位 → 主控 FPGA → 数码管和驱动 → 按键 → VGA 与排针,接插件全部贴板边。晶振离主控时钟引脚越近越好,底下不要走任何信号,周围用接地过孔包一圈。

关于接地,网上经常能搜到「星型接地」的做法,但需要分清楚场景:星型接地适合模拟与数字混合、且工作频率很低的板子,做法是所有模块的地线各自独立走回电源入口,只在那一点汇合,避免大电流在地平面上产生压降串到小信号里。而纯数字的 FPGA 载板,优先做法是底层铺完整地平面、顶层的零散地用过孔就近打下去,完整地平面对回流路径更友好。课程设计的板子基本属于后者,别为了「看起来专业」把地平面割得七零八落。

走线类型建议线宽备注
普通信号8~10 mil3W 原则,间距 ≥ 3 倍线宽
电源 5V / 3.3V20~30 mil按 1A/mm² 粗估电流能力
时钟线10 mil尽量短、直、不跨分割
数码管段选汇流15 mil多位同时点亮时电流叠加

去耦电容用 100nF,距离对应电源引脚不超过 5mm,先过电容再进引脚;电源入口再放一颗 10μF 以上的电解或钽电容兜底。如果后续要上六层板(蓝桥杯 EDA 板级设计里常见),叠层按 信号/地/信号/信号/电源/信号 排,把完整地放在第二层,时钟和高频信号走第一层,紧邻参考平面。

3.4 位号图、DRC 与打样前的自查

位号图是手工焊接时的对照图,也是答辩材料里很能加分的图。在 PCB 界面执行制造 → 导出 → 装配图 / 位号图,导出 PDF 或 PNG,注意导出前把丝印层的位号调到不压焊盘、字体不小于 0.6mm,否则打出来看不清。

打样前跑一遍 DRC,按免费工艺的常见下限设:线宽/间距 6/6 mil,最小孔径 0.3mm,过孔盖油,板框闭合。顺手用二十行脚本把 BOM 核一遍,比肉眼翻表格可靠。

# 核对导出的 BOM:位号重复、缺封装、缺型号 import csv, collections, sys bad = [] rows = list(csv.DictReader(open(sys.argv[1], encoding='utf-8-sig'))) seen = collections.Counter() for r in rows: ref = (r.get('Designator') or '').strip() foot = (r.get('Footprint') or '').strip() part = (r.get('Comment') or '').strip() for one in ref.split(','): # 一个 BOM 行可能含多个位号 seen[one.strip()] += 1 if not foot: bad.append(f'{ref} 缺封装') if not part: bad.append(f'{ref} 缺型号') dup = [k for k, v in seen.items() if v > 1] print('重复位号:', dup or '无') print('问题行:', bad or '无')

脚本只做三件最要命的检查:位号是否被两个元件共用(贴片时会漏焊一个)、封装是否为空(下单会报错)、型号是否为空(买错料)。导出 BOM 时字段名按嘉立创EDA 的默认表头来,如果用的是自己整理的表头,把DesignatorFootprintComment三个键名改成实际列名即可。

4. 游戏机上板调试:按键消抖、时序约束与引脚对不上怎么查

板子回来、代码综合通过,第一次上电往往什么都不显示。这一章按「先让时钟动起来、再让按键可靠、最后让时序收敛」的顺序把问题拆开。

4.1 按键消抖与手感参数

机械按键的抖动持续 5~20ms,不处理的话一次按下会被识别成好几帧输入,游戏里就是「一按跳三格」。标准做法是用主时钟打一个 20ms 计数器,稳定后输出,再取上升沿。

// 20ms 消抖 + 单周期上升沿,主时钟 50MHz module debounce #( parameter integer CLK_HZ = 50_000_000, parameter integer MS = 20 )( input wire clk, rst_n, key_in, output wire key_press // 按下瞬间输出一个时钟周期高 ); localparam integer CNT_MAX = CLK_HZ / 1000 * MS; // 1000000 reg [1:0] sync; // 两级同步,先治亚稳态 reg [31:0] cnt; reg stable, stable_d; always @(posedge clk or negedge rst_n) begin if (!rst_n) sync <= 2'b11; else sync <= {sync[0], key_in}; // 按键低有效 end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 0; stable <= 1'b1; end else if (sync[1] != stable) begin if (cnt == CNT_MAX - 1) begin cnt <= 0; stable <= sync[1]; end else cnt <= cnt + 1'b1; end else cnt <= 0; end always @(posedge clk or negedge rst_n) if (!rst_n) stable_d <= 1'b1; else stable_d <= stable; assign key_press = stable_d & ~stable; // 低有效按键的按下沿 endmodule

MS参数是手感旋钮:20ms 稳但偏钝,10ms 更跟手但有极小概率漏检。如果游戏需要长按连续移动,不要靠重复触发消抖模块,另加一个独立的自动重复计数器(比如按住 300ms 后每 80ms 发一次脉冲),这样点按和长按的行为可以分别调。

4.2 时序约束与跨时钟域

综合能过不等于时序能过。Quartus 里在 TimeQuest 用 SDC 约束,Vivado 里在 XDC 里写,思路一样:先声明主时钟,再让工具自动推导 PLL 出来的像素时钟,最后约束输出到 VGA 和数码管的延迟。

# 主时钟 50MHz,周期 20ns create_clock -name clk50 -period 20.000 [get_ports {clk}] # 自动推导 PLL 输出时钟与不确定性 derive_pll_clocks derive_clock_uncertainty # 输出到 VGA 与数码管的路径留 4ns 余量 set_output_delay -clock clk50 -max 4.0 \ [get_ports {vga_r[*] vga_g[*] vga_b[*] vga_hs vga_vs seg[*] sel[*]}]

编完在报告里看三个数:Fmax是否高于 50MHz 且有余量、有没有unconstrained path、跨时钟域的路径有没有被正确识别。前 2.1 节强调过全程单时钟域,就是为了让这里省事——只声明一个时钟,几乎不会出现跨域问题。唯一例外是按键输入,它相对时钟是异步的,用两级同步器处理(见 4.1 的sync)即可,不需要额外写set_false_path

4.3 引脚约束与「引脚与焊盘未对应」的定位

引脚分配错了,最典型的表现是「程序下载成功但没反应」。约束文件按下面的格式写,或者在 Pin Planner 里逐个点。

# Quartus 的 QSF 片段:约束引脚与 IO 电平 set_location_assignment PIN_23 -to clk set_location_assignment PIN_25 -to rst_n set_location_assignment PIN_28 -to key[0] set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to clk set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to key[0]
现象大概率原因定位动作
下载成功但全无反应时钟引脚约束错或晶振没起振把时钟引到一个 LED 上做分频闪烁,先验证时钟
按键响应的是别的功能按键引脚与约束表错位逐个按键点亮对应 LED,对照原理图
数码管段码全亮或全暗共阴/共阳判断反,或位选极性反只驱动一位,量电流方向
现象与 PCB 网络对不上符号引脚编号与封装焊盘号不一致回工程里开网络面板核对飞线

最后一种最难查,因为原理图看起来完全正确。判断方法很直接:在嘉立创EDA 里打开 PCB,点一下出问题的那颗器件,看网络面板里连到的引脚名,再对比规格书的实际脚位。如果符号脚位和实物不符,问题在符号库,不在布线,改符号后重新同步原理图即可。

5. 游戏机的参数化收尾:从课程设计到蓝桥杯EDA 板级工程的整理

课程设计做完只用一次,但很多同学会顺手拿去准备蓝桥杯 EDA 板级设计,两个方向共用一套工程。这里讲几个把工程「收干净」的做法,顺便把参数集中管理。

第一件事是把所有可调参数抽到一个头文件里,用 include 引进来。

// game_cfg.vh —— 全工程的参数唯一来源 `ifndef GAME_CFG_VH `define GAME_CFG_VH `define CLK_HZ 50_000_000 `define FRAME_HZ 60 `define H_ACTIVE 640 `define V_ACTIVE 480 `define BALL_SZ 8 `define BALL_SPEED 1 // 每帧移动像素数,可调难度 `define DB_MS 15 // 消抖时间,手感旋钮 `endif

综合脚本里加一行+incdir指向工程根目录,改成 800×600 只需要动H_ACTIVEV_ACTIVE和 VGA 时序参数,游戏逻辑一行都不用碰。这一步在答辩现场调难度时非常实用。

第二件事是图形资源上 Block RAM。打地鼠的图案、字体的点阵、开场画面,都用$readmemh.mif.hex初始化一块 ROM,比写一堆case语句清爽得多,也不占逻辑资源。

// 64x64 的图案 ROM,初始化文件由脚本生成 reg [15:0] sprite_rom [0:4095]; initial $readmemh("mole.hex", sprite_rom); wire [11:0] sprite_addr = py_in_sprite * 64 + px_in_sprite; wire [15:0] sprite_color = sprite_rom[sprite_addr];

调试期还有个很省事的办法:把游戏关键状态(得分、球坐标低 8 位、当前状态机状态)分别引到一个 8 位数码管或者带内嵌逻辑分析仪的探针上。Quartus 用 SignalTap、Vivado 用 ILA,抓十几个信号以内不会影响时序收敛,比反复改代码加 LED 快得多。

如果目标是蓝桥杯 EDA 板级设计,工程的完整度比功能花哨更重要:原理图库和封装库要整理成自己的库、BOM 里型号规格齐全、位号图和装配图导成 PDF、DRC 记录留档。比赛评审看的是这块板能不能被稳定制造,而不是球反弹得准不准。

最后一条实操建议:把game_cfg.vh、综合脚本和 pin 约束放在同一个目录,用 Makefile 或一条 bash 串起来,make sim跑仿真、make bit出比特流。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 18:16:21

Vue3 + TypeScript 项目命名规范:从组合式 API 到代码可维护性实践

写代码三年&#xff0c;最怕的不是业务复杂&#xff0c;是同事变量命名全靠当天心情。尤其项目切到 Vue3 TypeScript 之后&#xff0c;组合式 API 把一堆变量和方法全暴露在 setup 里&#xff0c;一个页面看下来&#xff0c;什么data1、res2、form、getData满天飞&#xff0c;…

作者头像 李华
网站建设 2026/9/18 18:16:09

Qt树形菜单开发实战:从QTreeWidget到QTreeView的选型与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 18:12:18

CentOS 7.9 MINI镜像挂载失败原因与实战解决方案

1. 这不是系统bug&#xff0c;是MINI镜像的“精简逻辑”在说话CentOS 7.9 MINI系统安装后无法挂载磁盘——这句话在运维群、技术论坛和装机新手的求助帖里高频出现&#xff0c;几乎成了MINI镜像落地的第一道坎。但我要先说清楚&#xff1a;这不是CentOS本身的问题&#xff0c;也…

作者头像 李华
网站建设 2026/9/18 18:12:06

Visual Studio中C++多源文件独立运行的三种实操方案

1. 项目概述&#xff1a;为什么“多个源文件分开运行”是个伪命题&#xff0c;但却是新手最真实的痛点在 Visual Studio&#xff08;VS&#xff09;里点开一个 C 项目&#xff0c;看到七八个.cpp文件堆在解决方案资源管理器里&#xff0c;心里就发毛&#xff1a;“我改了main.c…

作者头像 李华