简介:这份燕山大学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 endmoduleDIV用整数除法取整,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_x、hit_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 MHz | 800×525×60 ≈ 25.2M |
| H_SYNC / H_BP / H_ACT / H_FP | 96 / 48 / 640 / 16 | 合计 800 |
| V_SYNC / V_BP / V_ACT / V_FP | 2 / 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 个周期出现一次;vx在ball_x <= 12时是否翻转;h_act的占空比是否是 640/800。这三条过了,上板基本不会出现完全没图像的尴尬。
3. 用嘉立创EDA 画游戏机载板:原理图、网络联动与打样前检查
很多课程设计只做到开发板插杜邦线,演示时按键一按就复位,非常吃亏。花两天画一块载板,把按键、数码管、VGA 接口、晶振都做到一块板子上,演示稳定性完全不一样,答辩的工程完整度也上去了。嘉立创EDA 免费、库全、能直接下单打样,是这条路线里最省事的选择。
3.1 元件与封装:先解决「元件的引脚与焊盘未对应」
画板子最常见的翻车点不是布线,是符号引脚编号和封装焊盘编号对不上。自建符号时,符号里每个引脚的「编号」必须和 PCB 封装的焊盘号严格一致,名称可以随便写,编号错一位就会导致网络连到错误的焊盘上,而 DRC 是查不出来的。
| 元件 | 推荐封装 | 易错点 |
|---|---|---|
| 轻触按键 6×6 | SW-SMD_4P 或 THT 4 脚 | 四脚按键常有两两内部短接,不要当独立四路用 |
| 共阴数码管 | 0.36 寸 4 位直插 | 共阴/共阳选错,段码全反 |
| VGA 母座 DB15 | DB15-HD 直插带螺母 | 引脚 1/2/3 是 R/G/B,13/14 是行场同步 |
| 有源晶振 50MHz | SMD 5032 四脚 | 引脚 1 是使能或悬空,别接成 GND |
| 去耦电容 100nF | 0402/0603 | 每个电源引脚旁边一颗,别共用 |
自建符号的正确顺序是:先确定实际买到手的器件型号和规格书,按脚位画符号,再从立创商城搜索同型号的现成封装复用;找不到就自己按规格书画焊盘,画完用「封装管理器」逐个核对编号。这一步省下的两小时,通常要用一次打样报废来还。
3.2 原理图到 PCB 的网络联动怎么设
嘉立创EDA 的原理图和 PCB 是强关联的,改完原理图必须显式同步,否则板子上的网络还是旧的。常见做法是:
- 原理图里执行
设计 → 检查 DRC,先把单端网络、重复位号清掉; - 执行
设计 → 转换/更新原理图到 PCB,新建工程选「转换原理图到 PCB」,已有 PCB 选「更新 PCB」; - 弹窗里勾选「保留已有布局」,这样按位号匹配,已经摆好的元件不会被重新扔到板框外;
- 更新完成后打开
设计 → 网络面板,确认新增的网络都在,且没有出现「未连接」的红色飞线。
| 现象 | 原因 | 处理 |
|---|---|---|
| 更新后元件全跑到板框外 | 位号被重排,无法按位号匹配 | 撤销,先在原理图里锁定位号再更新 |
| PCB 上多了几个孤立焊盘 | 原理图符号引脚编号与封装焊盘号不一致 | 回到 3.1 核对编号 |
网络面板里网络名带$1N | 网络没有命名,属于隐式连接 | 在原理图里给关键网络手动命名 |
| 飞线全消失但走线还在 | 原理图删了器件但没同步 | 重新执行更新,选择删除多余器件 |
改完网络后飞线会自动重算,但已经布好的走线不会自动跟着动。所以布局布线之前,一定要把原理图的网络关系冻结,中途改网络等于返工。
3.3 布局顺序、去耦和地平面:星型接地什么时候才用
布局按信号流走:电源入口和稳压 → 晶振与复位 → 主控 FPGA → 数码管和驱动 → 按键 → VGA 与排针,接插件全部贴板边。晶振离主控时钟引脚越近越好,底下不要走任何信号,周围用接地过孔包一圈。
关于接地,网上经常能搜到「星型接地」的做法,但需要分清楚场景:星型接地适合模拟与数字混合、且工作频率很低的板子,做法是所有模块的地线各自独立走回电源入口,只在那一点汇合,避免大电流在地平面上产生压降串到小信号里。而纯数字的 FPGA 载板,优先做法是底层铺完整地平面、顶层的零散地用过孔就近打下去,完整地平面对回流路径更友好。课程设计的板子基本属于后者,别为了「看起来专业」把地平面割得七零八落。
| 走线类型 | 建议线宽 | 备注 |
|---|---|---|
| 普通信号 | 8~10 mil | 3W 原则,间距 ≥ 3 倍线宽 |
| 电源 5V / 3.3V | 20~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 的默认表头来,如果用的是自己整理的表头,把Designator、Footprint、Comment三个键名改成实际列名即可。
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; // 低有效按键的按下沿 endmoduleMS参数是手感旋钮: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_ACTIVE、V_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出比特流。
本文还有配套的精品资源,点击获取